@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
package/src/fields.tsx ADDED
@@ -0,0 +1,942 @@
1
+ // The form fields of the editor, drawn from the field lists in collections.ts. Lists and groups
2
+ // (object) hold other fields, so the deep front page is the same code as a price. A list of blocks
3
+ // (a fixed page's sections) is a list whose items each have a `type` that gives their fields; it is
4
+ // edited as a text with the sections as cards in it (SectionsField.tsx).
5
+ import { createContext, useContext, useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react';
6
+ import { ArrowDown, ArrowUp, CaretDown, PencilSimple, Plus, Trash } from '@phosphor-icons/react';
7
+ import {
8
+ ArrowLink,
9
+ Button,
10
+ Checkbox,
11
+ ChipField,
12
+ FieldGroup,
13
+ FoldItem,
14
+ FoldList,
15
+ DatePicker,
16
+ DateTimeField,
17
+ FormField,
18
+ type InField,
19
+ IconButton,
20
+ IconPicker,
21
+ ImagePick,
22
+ Label,
23
+ Group,
24
+ listClass,
25
+ MultiChoice,
26
+ SupportingText,
27
+ PhoneField,
28
+ SegmentedButton,
29
+ Select,
30
+ SheetActions,
31
+ SheetBody,
32
+ SheetLink,
33
+ BottomSheet,
34
+ SheetTitle,
35
+ ShownValue,
36
+ subListClass,
37
+ SwitchRow,
38
+ TextArea,
39
+ TextField,
40
+ ProblemNote,
41
+ type Pickable,
42
+ ListItem,
43
+ } from './ui';
44
+ import type { Edit, Field, Relation, Template } from './schema';
45
+ import type { SiteData } from '@site/editor';
46
+ import { ask, closeChanged } from './ask';
47
+ import MarkdownEditor, { type MarkdownHandle } from './MarkdownEditor';
48
+ import { IMAGE, useBlockDialog } from './BlockDialog';
49
+ import PagePicker, { PageListItem } from './PagePicker';
50
+ import SectionsField from './SectionsField';
51
+ import { leaveTo } from './FormFrame';
52
+ import { shownAt } from './images';
53
+ import { addressOf } from './addresses';
54
+ import { clean, inPairs, inUse, newItem, phoneParts, phoneText, problemAt, problemOf, type Around, type Problem } from './check';
55
+ import { Icon } from './icon';
56
+ import { t } from './i18n';
57
+ import { snackbar } from '../lib/snackbar';
58
+
59
+ /** A choice of a relation. `on`: where it is shown on the site (a promotion), for a field with `onPage`; `text`: what a
60
+ * button that opens it says when it has no text of its own, or `as`: the target of the buttons (`buttonTargets`) whose
61
+ * text it has (Opens.as). */
62
+ /** A page or post may have its status, whether the menu links to it and its date, for the order and filters of the sheet
63
+ * it is picked in (usePickTools). */
64
+ export type Option = Pickable & { on?: { everywhere: boolean; pages: string[] }; text?: string; as?: string };
65
+ export type { PageState } from './ui';
66
+
67
+ /** The choices shown on a page (its address), or on every page when no page is edited (Field.onPage). */
68
+ export const onThePage = (options: Option[], page?: string) =>
69
+ options.filter((o) => !o.on || o.on.everywhere || (!!page && o.on.pages.includes(page)));
70
+
71
+ // More choices than this, and the sheet with them has a search over them.
72
+ const manyChoices = 10;
73
+
74
+ // The rows of the site's lists (the standard blocks' and the apps', "Lister"), each as its file has it, by the list's
75
+ // id, in their order: what a block shows from a list (an app's block from the app's own list).
76
+ export type ListRows = Record<string, Record<string, unknown>[]>;
77
+
78
+ // A testimonial as its file in src/data/testimonials/ has it (the quote's paragraphs separated by a
79
+ // blank line), for the cards of the testimonial blocks.
80
+ export type TestimonialRow = {
81
+ slug: string;
82
+ name: string;
83
+ /** Its picture (optional), by the name. */
84
+ image?: string;
85
+ context?: string;
86
+ quote: string;
87
+ categories?: string[];
88
+ frontPage: boolean;
89
+ };
90
+
91
+ // What fields need from the editor: the choices for relations, the lists' rows and the testimonials, and
92
+ // pictures (the gallery, Media.tsx).
93
+ export type FieldContext = {
94
+ relations: Record<Relation, Option[]>;
95
+ lists: ListRows;
96
+ testimonials: TestimonialRow[];
97
+ /** The rest of the site, for the previews of the blocks (sitedata.ts). */
98
+ site: SiteData;
99
+ /** The address of the page being edited (a relation with `onPage` offers what is shown there). */
100
+ page?: string;
101
+ /** Asks for a picture from the gallery or a new one (useMediaPicker); its address on the site. Also
102
+ * for a text (MarkdownEditor); without it, no picture button. */
103
+ pickImage?: () => Promise<string | null>;
104
+ /** The address of a field's page of its own (Field.page), by its name, in a form that has them (CompanyForm.tsx). */
105
+ pageOf?: (field: string) => string;
106
+ };
107
+
108
+ export const Fields = createContext<FieldContext | null>(null);
109
+
110
+ type Props = { field: Field; value: unknown; onChange: (value: unknown) => void };
111
+
112
+ // Where a field is in what is edited: the names and item numbers to it joined with dots
113
+ // ("items.0.title"), as problemAt in check.ts names the field a problem is about.
114
+ const PathHere = createContext('');
115
+ const joined = (base: string, name: string | number) => (base ? `${base}.${name}` : String(name));
116
+
117
+ /** The field that keeps a form from being saved (problemAt in check.ts), when it was found (a new
118
+ * number each time, so it is shown again on the next try), and how to stop showing it: the item of a
119
+ * list it is in is opened, it is scrolled to and its border flashes (.editor-wrong in editor.css),
120
+ * until it is changed. Every form has it (useWrongField). */
121
+ export type Wrong = { path: string; at: number; clear: () => void };
122
+ export const WrongField = createContext<Wrong | null>(null);
123
+
124
+ /** A form that cannot be saved as it is: the reason as a message at the front (`say`, the form's
125
+ * notify; also over a sheet) and the field marked (WrongField, which the form's fields are put
126
+ * inside). `stop(problem)` does both and is true when there is a problem, so a save can end there:
127
+ * `if (stop(problemAt(fields, values))) return;`. The same in every form. `open`: opens what the field is in (a folded
128
+ * section, FoldSection in ui.tsx), in the same go, so the field is shown when it is scrolled to. */
129
+ export function useWrongField(say: (text: string) => void, open?: (path: string) => void) {
130
+ const [wrong, setWrong] = useState<{ path: string; at: number } | null>(null);
131
+ const field = useMemo<Wrong | null>(() => wrong && { ...wrong, clear: () => setWrong(null) }, [wrong]);
132
+ const stop = (problem: Problem | null) => {
133
+ if (problem) open?.(problem.path);
134
+ setWrong(problem && { path: problem.path, at: Date.now() });
135
+ if (problem) say(problem.text);
136
+ return !!problem;
137
+ };
138
+ return { field, stop };
139
+ }
140
+
141
+ // A field marked when it keeps the form from being saved (WrongField): its path, the element to mark (`box`), and its
142
+ // change, which ends the marking.
143
+ function useMarked<T extends HTMLElement>(props: Props) {
144
+ const path = joined(useContext(PathHere), props.field.name);
145
+ const wrong = useContext(WrongField);
146
+ const here = wrong?.path === path;
147
+ const box = useRef<T>(null);
148
+ // The class is set here, not by React, so the flashing starts again on each try.
149
+ useEffect(() => {
150
+ const el = box.current;
151
+ if (!el) return;
152
+ el.classList.remove('editor-wrong');
153
+ if (!here) return;
154
+ void el.offsetWidth;
155
+ el.classList.add('editor-wrong');
156
+ el.scrollIntoView({ block: 'center', behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
157
+ }, [here, wrong?.at]);
158
+ // A change in the marked field ends the marking.
159
+ const onChange = (v: unknown) => {
160
+ if (here) wrong.clear();
161
+ props.onChange(v);
162
+ };
163
+ return { path, box, onChange };
164
+ }
165
+
166
+ export function FieldInput(props: Props) {
167
+ const { path, box, onChange } = useMarked<HTMLDivElement>(props);
168
+ return (
169
+ <PathHere.Provider value={path}>
170
+ <div ref={box}>
171
+ <FieldControl {...props} onChange={onChange} />
172
+ </div>
173
+ </PathHere.Provider>
174
+ );
175
+ }
176
+
177
+ // The pages a field that picks a page (`relation` to pages) offers, in groups: with `fixed`, the fixed pages first,
178
+ // without the ones that are also a page (Om mig).
179
+ function pageGroups(field: Field, context: FieldContext, options = context.relations[field.to!]) {
180
+ const fixed = field.fixed ? context.relations.fixed.filter((f) => !options.some((o) => o.value === f.value)) : [];
181
+ return fixed.length
182
+ ? [
183
+ { title: t.forms.fixedPages, options: fixed },
184
+ { title: t.forms.pages, options },
185
+ ]
186
+ : [{ title: t.forms.pages, options }];
187
+ }
188
+
189
+ /** Fields that pick a page (`relation` to pages) as the rows of a list (Group), each with its name and the page picked,
190
+ * that open the sheet with the pages, titled with its name and its hint under it (PageListItem): "Standardsider"
191
+ * (Part.rows). Marked as the other fields are (WrongField). */
192
+ export function FieldRows({
193
+ fields,
194
+ value,
195
+ onChange,
196
+ }: {
197
+ fields: Field[];
198
+ value: Record<string, unknown>;
199
+ onChange: (v: Record<string, unknown>) => void;
200
+ }) {
201
+ return (
202
+ <Group>
203
+ {fields.map((f) => (
204
+ <FieldRow key={f.name} field={f} value={value[f.name]} onChange={(v) => onChange({ ...value, [f.name]: v })} />
205
+ ))}
206
+ </Group>
207
+ );
208
+ }
209
+
210
+ function FieldRow(props: Props) {
211
+ const { box, onChange } = useMarked<HTMLButtonElement>(props);
212
+ const { field, value } = props;
213
+ return (
214
+ <PageListItem
215
+ ref={box}
216
+ title={field.label}
217
+ hint={field.hint}
218
+ value={value == null ? '' : String(value)}
219
+ groups={pageGroups(field, useContext(Fields)!)}
220
+ required={field.required}
221
+ onChange={onChange}
222
+ />
223
+ );
224
+ }
225
+
226
+ function FieldControl({ field, value, onChange }: Props) {
227
+ const context = useContext(Fields)!;
228
+ // A field of the form itself, not one in a group or a list (a field's page, Field.page).
229
+ const own = useContext(PathHere) === field.name;
230
+ const id = useId();
231
+ const text = value == null ? '' : String(value);
232
+ // What is wrong with the text, shown under it once it has been typed.
233
+ const [wrong, setWrong] = useState<string | null>(null);
234
+ // A phone number's country code (Field.phone), the last it had: kept when the number is emptied,
235
+ // and when it is picked before the number is typed.
236
+ const [pickedCode, setPickedCode] = useState(() => (field.phone && text.trim() ? phoneParts(field, text)[0] : undefined));
237
+ // What a field to write or choose in says in itself, as the theme's text field does (its name on its edge, what it
238
+ // does or what is wrong under it, how much it should have at most); the other fields have their name over them.
239
+ const inField: InField = { label: field.label, hint: hintOf(field, text), problem: wrong ? `${wrong}.` : undefined, limit: field.max };
240
+ let inside = false;
241
+ let control;
242
+
243
+ switch (field.type) {
244
+ case 'boolean':
245
+ if (field.toggle) {
246
+ const on = value === true;
247
+ return <SwitchRow label={field.label} hint={hintOf(field)} selected={on} texts={field.toggle} onChange={onChange} />;
248
+ }
249
+ return <Checkbox label={field.label} hint={hintOf(field)} checked={value === true} onChange={onChange} />;
250
+ case 'object':
251
+ return (
252
+ <FieldGroup label={field.label} hint={hintOf(field)}>
253
+ <FieldList fields={field.fields!} value={(value ?? {}) as Record<string, unknown>} onChange={onChange} />
254
+ </FieldGroup>
255
+ );
256
+ case 'list':
257
+ return <ListInput field={field} value={(value ?? []) as unknown[]} onChange={onChange} />;
258
+ case 'blocks':
259
+ return <SectionsField field={field} value={(value ?? []) as Item[]} onChange={onChange} />;
260
+ case 'markdown':
261
+ control = field.template ? (
262
+ <MarkdownWithBlocks value={text} template={field.template} onChange={onChange} />
263
+ ) : (
264
+ <MarkdownField value={text} onChange={onChange} />
265
+ );
266
+ break;
267
+ case 'readonly':
268
+ control = <ShownValue muted>{text || t.forms.fromTitle}</ShownValue>;
269
+ break;
270
+ case 'text':
271
+ inside = true;
272
+ control = (
273
+ <TextArea
274
+ id={id}
275
+ {...inField}
276
+ rows={4}
277
+ value={text}
278
+ placeholder={field.placeholder?.(context.site)}
279
+ onChange={(e) => onChange(e.target.value)}
280
+ />
281
+ );
282
+ break;
283
+ case 'date':
284
+ inside = true;
285
+ control = <DatePicker id={id} {...inField} value={text} onChange={onChange} />;
286
+ break;
287
+ // A date and a time, "2026-03-01T12:00" (Danish time, as the phone or computer shows it).
288
+ case 'datetime':
289
+ inside = true;
290
+ control = <DateTimeField id={id} {...inField} value={text} onChange={onChange} />;
291
+ break;
292
+ case 'number':
293
+ inside = true;
294
+ control = (
295
+ <TextField
296
+ id={id}
297
+ {...inField}
298
+ type="number"
299
+ inputMode="numeric"
300
+ min={0}
301
+ value={text}
302
+ onChange={(e) => onChange(e.target.value === '' ? '' : Number(e.target.value))}
303
+ />
304
+ );
305
+ break;
306
+ case 'select': {
307
+ // An option that needs a choice on the page (Field.options' onPage) only while there is one, or it is chosen.
308
+ const offered = field.options!.filter(
309
+ (o) => !o.onPage || o.value === text || onThePage(context.relations[o.onPage] ?? [], context.page).length > 0,
310
+ );
311
+ if (field.locked) {
312
+ // The kind of an entry: what it is, not changed here (its hint under the label says more).
313
+ const chosen = field.options!.find((o) => o.value === text) ?? (text ? undefined : field.options![0]);
314
+ control = (
315
+ <ShownValue id={id} icon={chosen?.icon}>
316
+ {chosen?.label ?? text}
317
+ </ShownValue>
318
+ );
319
+ break;
320
+ }
321
+ if (field.segments) {
322
+ // Without a value (saved before the field was there), its default, as the fields that depend on it have it (inUse).
323
+ const shown = text || String(field.default ?? '');
324
+ control = <SegmentedButton label={field.label} value={shown} options={offered} onChange={onChange} />;
325
+ break;
326
+ }
327
+ if (field.icons) {
328
+ // The chosen icon, and all of them to tap (IconPicker).
329
+ const choices = [...(field.required ? [] : [{ label: t.forms.noIcon, value: '' }]), ...field.options!];
330
+ control = (
331
+ <IconPicker
332
+ id={id}
333
+ label={field.label}
334
+ value={text}
335
+ options={choices}
336
+ icon={(name, size) => <Icon name={name} size={size} />}
337
+ onChange={onChange}
338
+ />
339
+ );
340
+ break;
341
+ }
342
+ {
343
+ // Not chosen ("Ingen"), unless a choice is itself the empty value (the price card's "Book en tid").
344
+ const none = !field.required && !offered.some((o) => o.value === '');
345
+ const options = none ? [{ value: '', label: t.forms.noChoice }, ...offered] : offered;
346
+ inside = true;
347
+ control = (
348
+ <Select
349
+ id={id}
350
+ {...inField}
351
+ title={field.label}
352
+ value={text}
353
+ options={options}
354
+ empty={t.forms.choose}
355
+ search={options.length > manyChoices ? t.common.searchIn(field.label) : undefined}
356
+ href={field.page && own && context.pageOf ? context.pageOf(field.name) : undefined}
357
+ onChange={onChange}
358
+ />
359
+ );
360
+ }
361
+ break;
362
+ }
363
+ case 'relation': {
364
+ const options = field.onPage ? onThePage(context.relations[field.to!] ?? [], context.page) : context.relations[field.to!];
365
+ if (field.multiple) {
366
+ // With `fixed`, the fixed pages first (without the ones that are also a page), as below.
367
+ const all =
368
+ field.to === 'pages' && field.fixed
369
+ ? [...context.relations.fixed.filter((f) => !options.some((o) => o.value === f.value)), ...options]
370
+ : options;
371
+ control = <MultiRelation field={field} options={all} value={(value ?? []) as string[]} onChange={onChange} />;
372
+ break;
373
+ }
374
+ if (field.to === 'pages' || field.to === 'standardPages') {
375
+ inside = true;
376
+ control = (
377
+ <PagePicker
378
+ id={id}
379
+ inField={inField}
380
+ value={text}
381
+ groups={pageGroups(field, context, options)}
382
+ required={field.required}
383
+ onChange={onChange}
384
+ />
385
+ );
386
+ break;
387
+ }
388
+ // A value that is not among the choices (a removed page) is still shown, so it is not lost silently.
389
+ const missing = text && !options.some((o) => o.value === text);
390
+ const choices = [
391
+ ...(field.required ? [] : [{ value: '', label: t.forms.noChoice }]),
392
+ ...(missing ? [{ value: text, label: t.forms.missing(text) }] : []),
393
+ ...options,
394
+ ];
395
+ inside = true;
396
+ control = (
397
+ <Select
398
+ id={id}
399
+ {...inField}
400
+ title={field.label}
401
+ value={text}
402
+ options={choices}
403
+ empty={t.forms.choose}
404
+ search={choices.length > manyChoices ? t.common.searchIn(field.label) : undefined}
405
+ onChange={onChange}
406
+ />
407
+ );
408
+ break;
409
+ }
410
+ case 'image':
411
+ control = (
412
+ <ImagePick
413
+ id={id}
414
+ src={text ? shownAt(text) : undefined}
415
+ onPick={async () => {
416
+ const src = await context.pickImage?.();
417
+ if (src) onChange(src);
418
+ }}
419
+ onRemove={field.required ? undefined : () => onChange('')}
420
+ />
421
+ );
422
+ break;
423
+ default: {
424
+ if (field.chips) {
425
+ control = (
426
+ <ChipField
427
+ id={id}
428
+ value={text
429
+ .split(',')
430
+ .map((w) => w.trim())
431
+ .filter(Boolean)}
432
+ one={field.chips === 'one'}
433
+ onChange={(words) => onChange(words.join(', '))}
434
+ />
435
+ );
436
+ break;
437
+ }
438
+ // A phone number: its country code in the button before it (PhoneField), the number in the field.
439
+ // Typed with a code before it (+46), the code goes to the button once it is one.
440
+ const [code, number] = field.phone ? phoneParts(field, text) : ['', text];
441
+ const shownCode = text.trim() ? code : (pickedCode ?? code);
442
+ const full = (typed: string) => (field.phone ? phoneText(field, shownCode, typed) : typed);
443
+ inside = true;
444
+ control = (
445
+ <TextField
446
+ id={id}
447
+ {...inField}
448
+ value={number}
449
+ placeholder={field.placeholder?.(context.site)}
450
+ inputMode={field.digitsInPairs ? 'tel' : field.type === 'string' && field.name === 'email' ? 'email' : undefined}
451
+ onChange={(e) => {
452
+ const typed = full(e.target.value);
453
+ onChange(typed);
454
+ if (field.phone && typed.trim()) setPickedCode(phoneParts(field, typed)[0]);
455
+ // Once shown, the problem goes as soon as it is put right.
456
+ if (wrong) setWrong(problemOf(field, typed.trim()));
457
+ }}
458
+ // A text with a form is checked once it is typed (without spaces around it), and a number
459
+ // in pairs is written so.
460
+ onBlur={
461
+ field.pattern || field.digitsInPairs || field.address
462
+ ? () => {
463
+ const typed = field.digitsInPairs
464
+ ? inPairs(field, text)
465
+ : field.address
466
+ ? addressOf(text, field.address) || text.trim()
467
+ : text.trim();
468
+ if (typed !== text) onChange(typed);
469
+ setWrong(problemOf(field, typed));
470
+ }
471
+ : undefined
472
+ }
473
+ />
474
+ );
475
+ if (field.phone)
476
+ control = (
477
+ <PhoneField
478
+ code={shownCode}
479
+ onCode={(c) => {
480
+ setPickedCode(c);
481
+ if (number.trim()) {
482
+ const typed = inPairs(field, phoneText(field, c, number));
483
+ onChange(typed);
484
+ setWrong(problemOf(field, typed));
485
+ }
486
+ }}
487
+ >
488
+ {control}
489
+ </PhoneField>
490
+ );
491
+ }
492
+ }
493
+
494
+ if (inside) return control;
495
+ return (
496
+ <FormField
497
+ label={field.label}
498
+ htmlFor={id}
499
+ hint={hintOf(field, text)}
500
+ problem={wrong ? { id: `${id}-problem`, text: wrong } : undefined}
501
+ count={field.max ? { length: text.length, max: field.max } : undefined}
502
+ >
503
+ {control}
504
+ </FormField>
505
+ );
506
+ }
507
+
508
+ // The object the fields being shown are in (an item of a list: the object with the list), for a field
509
+ // that depends on one there (inUse).
510
+ const AroundHere = createContext<Around | undefined>(undefined);
511
+
512
+ // The fields of an object, in order.
513
+ export function FieldList({
514
+ fields,
515
+ value,
516
+ onChange,
517
+ }: {
518
+ fields: Field[];
519
+ value: Record<string, unknown>;
520
+ onChange: (v: unknown) => void;
521
+ }) {
522
+ const around = useContext(AroundHere);
523
+ return (
524
+ <AroundHere.Provider value={{ fields, value, around }}>
525
+ {fields
526
+ .filter((f) => inUse(f, fields, value, around))
527
+ .map((f) => (
528
+ <FieldInput key={f.name} field={f} value={value[f.name]} onChange={(v) => onChange({ ...value, [f.name]: v })} />
529
+ ))}
530
+ </AroundHere.Provider>
531
+ );
532
+ }
533
+
534
+ // Text with formatting, in the same editor as the pages (MarkdownEditor), saved as Markdown.
535
+ function MarkdownField({ value, onChange }: { value: string; onChange: (v: unknown) => void }) {
536
+ const editor = useRef<MarkdownHandle>(null);
537
+ return (
538
+ <MarkdownEditor
539
+ ref={editor}
540
+ short
541
+ value={value}
542
+ onChange={() => {
543
+ const markdown = editor.current?.markdown();
544
+ if (markdown != null) onChange(markdown.trimEnd());
545
+ }}
546
+ />
547
+ );
548
+ }
549
+
550
+ // The same with the blocks its template allows (Field.template: a "Banner"'s picture and "Knapper"), put in with "+"
551
+ // as in a page's text, in the sheet of the blocks (BlockDialog.tsx) over the sheet or form the field is in. A picture
552
+ // only when the template has 'image', without a format or crop of its own.
553
+ function MarkdownWithBlocks({ value, template, onChange }: { value: string; template: Template; onChange: (v: unknown) => void }) {
554
+ const editor = useRef<MarkdownHandle>(null);
555
+ const context = useContext(Fields)!;
556
+ const [blocks, dialog] = useBlockDialog(
557
+ undefined,
558
+ { lists: context.lists, testimonials: context.testimonials, relations: context.relations, site: context.site, path: context.page },
559
+ template,
560
+ { crop: false },
561
+ );
562
+ const pictures = !template.blocks || template.blocks.includes(IMAGE);
563
+ return (
564
+ <>
565
+ <MarkdownEditor
566
+ ref={editor}
567
+ short
568
+ value={value}
569
+ blocks={blocks}
570
+ addImage={pictures ? context.pickImage : undefined}
571
+ onChange={() => {
572
+ const markdown = editor.current?.markdown();
573
+ if (markdown != null) onChange(markdown.trimEnd());
574
+ }}
575
+ />
576
+ {dialog}
577
+ </>
578
+ );
579
+ }
580
+
581
+ // Several picked from a list (a post's categories, the posts on the front page): the picked ones as
582
+ // pills like the filters in Filer, a tap on one takes it away, and a last pill ("Tilføj tag",
583
+ // Field.add) opens a sheet from the bottom with all the choices, where a tap picks one or takes it
584
+ // away again (the sheet stays open, so more can be picked). With many choices it has a search; at
585
+ // the bottom, the link to make a new one (Field.edit, "Opret et tag"). A picked value that is
586
+ // not among the choices (a removed category) is still shown, so it is not lost.
587
+ function MultiRelation({
588
+ field,
589
+ options,
590
+ value,
591
+ onChange,
592
+ }: {
593
+ field: Field;
594
+ options: Option[];
595
+ value: string[];
596
+ onChange: (v: unknown) => void;
597
+ }) {
598
+ return (
599
+ <MultiChoice
600
+ label={field.label}
601
+ options={options}
602
+ value={value}
603
+ add={field.add}
604
+ max={field.maxItems}
605
+ keep={field.to}
606
+ footer={
607
+ field.edit &&
608
+ ((close) => (
609
+ <SheetLink
610
+ href={`#${field.edit!.to}`}
611
+ onClick={(e) => {
612
+ e.preventDefault();
613
+ close();
614
+ leaveTo(`#${field.edit!.to}`);
615
+ }}
616
+ >
617
+ {field.edit!.text}
618
+ </SheetLink>
619
+ ))
620
+ }
621
+ onChange={onChange}
622
+ />
623
+ );
624
+ }
625
+
626
+ // What is written under a field's name: its hint, or the one of the option chosen (`value`), and the
627
+ // link to where it is edited instead (Field.edit). Several picked from a list have the link in their
628
+ // sheet instead (MultiRelation).
629
+ function hintOf(field: Field, value?: string): ReactNode {
630
+ const edit = field.type === 'relation' && field.multiple ? undefined : field.edit;
631
+ const hint = field.hint ?? field.options?.find((o) => o.value === value)?.hint;
632
+ if (!hint && !edit) return undefined;
633
+ return (
634
+ <>
635
+ {hint}
636
+ {hint && edit && ' '}
637
+ {edit && <EditLink edit={edit} />}
638
+ </>
639
+ );
640
+ }
641
+
642
+ // A link to another screen of the editor, which asks first while something is unsaved.
643
+ export function EditLink({ edit }: { edit: Edit }) {
644
+ return (
645
+ <ArrowLink
646
+ href={`#${edit.to}`}
647
+ onClick={(e) => {
648
+ e.preventDefault();
649
+ leaveTo(`#${edit.to}`);
650
+ }}
651
+ >
652
+ {edit.text}
653
+ </ArrowLink>
654
+ );
655
+ }
656
+
657
+ type Item = Record<string, unknown>;
658
+
659
+ // An item's title in a list (Field.summary): its field's value, of several the first filled in.
660
+ const summaryOf = (field: Field, item: unknown) =>
661
+ [field.summary ?? []]
662
+ .flat()
663
+ .map((name) => String((item as Item)?.[name] ?? '').trim())
664
+ .find(Boolean) ?? '';
665
+
666
+ // A list of items, each opened to its fields. `add={false}` leaves out "Tilføj …" below it, for a
667
+ // list that is added to with "+" in the top bar: an item added at the end, here or there, is opened
668
+ // and scrolled to. (The menu has its own, MenuList.)
669
+ export function ListInput({
670
+ field,
671
+ value,
672
+ onChange,
673
+ add: canAdd = true,
674
+ }: {
675
+ field: Field;
676
+ value: unknown[];
677
+ onChange: (v: unknown) => void;
678
+ add?: boolean;
679
+ }) {
680
+ const [open, setOpen] = useState<number | null>(null);
681
+ const list = useRef<HTMLUListElement>(null);
682
+ const length = useRef(value.length);
683
+ // The item with the field that keeps it from being saved is opened (WrongField).
684
+ const path = useContext(PathHere);
685
+ const wrong = useContext(WrongField);
686
+ useEffect(() => {
687
+ if (!wrong?.path.startsWith(`${path}.`)) return;
688
+ const i = Number(wrong.path.slice(path.length + 1).split('.')[0]);
689
+ if (Number.isInteger(i)) setOpen(i);
690
+ }, [wrong?.at]);
691
+ useEffect(() => {
692
+ if (value.length === length.current + 1) {
693
+ setOpen(value.length - 1);
694
+ requestAnimationFrame(() => list.current?.lastElementChild?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }));
695
+ }
696
+ length.current = value.length;
697
+ }, [value.length]);
698
+ const move = (i: number, to: number) => {
699
+ const next = [...value];
700
+ [next[i], next[to]] = [next[to], next[i]];
701
+ onChange(next);
702
+ if (open === i) setOpen(to);
703
+ };
704
+ const remove = async (i: number) => {
705
+ if (!(await ask(t.forms.removeQuestion(field.singular ?? t.forms.theItem), t.common.remove))) return;
706
+ onChange(value.filter((_, j) => j !== i));
707
+ setOpen(null);
708
+ };
709
+ const add = () => onChange([...value, newItem(field)]);
710
+ // A list of pages, posts …: each by its name.
711
+ const context = useContext(Fields);
712
+ const named = field.field?.type === 'relation' ? context?.relations[field.field.to!] : undefined;
713
+ const title = (item: unknown, i: number) =>
714
+ field.field
715
+ ? (named?.find((o) => o.value === item)?.label ?? String(item ?? '')).slice(0, 80)
716
+ : summaryOf(field, item) || t.forms.numbered(field.singular ?? t.forms.itemTitle, i + 1);
717
+ return (
718
+ <div>
719
+ <Label hint={hintOf(field)}>{field.label}</Label>
720
+ <FoldList ref={list}>
721
+ {value.map((item, i) => (
722
+ <FoldItem
723
+ key={i}
724
+ title={title(item, i) || t.forms.empty}
725
+ open={open === i}
726
+ onToggle={() => setOpen(open === i ? null : i)}
727
+ tools={
728
+ <>
729
+ <IconButton icon={ArrowUp} label={t.common.moveUp} disabled={i === 0} onClick={() => move(i, i - 1)} />
730
+ <IconButton icon={ArrowDown} label={t.common.moveDown} disabled={i === value.length - 1} onClick={() => move(i, i + 1)} />
731
+ <IconButton icon={Trash} label={t.common.remove} onClick={() => remove(i)} />
732
+ </>
733
+ }
734
+ >
735
+ <PathHere.Provider value={joined(path, i)}>
736
+ {field.field ? (
737
+ <FieldInput field={field.field} value={item} onChange={(v) => onChange(value.map((x, j) => (j === i ? v : x)))} />
738
+ ) : (
739
+ <FieldList
740
+ fields={field.fields!}
741
+ value={(item ?? {}) as Item}
742
+ onChange={(v) => onChange(value.map((x, j) => (j === i ? v : x)))}
743
+ />
744
+ )}
745
+ </PathHere.Provider>
746
+ </FoldItem>
747
+ ))}
748
+ </FoldList>
749
+ {/* No more than `maxItems` (the action bar's two buttons). */}
750
+ {canAdd && !(field.maxItems !== undefined && value.length >= field.maxItems) && (
751
+ <Button kind="outlined" size="small" icon={Plus} className="mt-2" onClick={add}>
752
+ {t.forms.addOne(field.singular ?? t.forms.item)}
753
+ </Button>
754
+ )}
755
+ </div>
756
+ );
757
+ }
758
+
759
+ const capitalFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
760
+
761
+ // The menu (DataForm.tsx): the items as rows in a list like the editor's other lists (listClass,
762
+ // ListItem), and the items under one (its list field, the submenu) indented along a line, shown with
763
+ // the arrow at the right. An item's text and page are changed in a sheet from the bottom,
764
+ // opened with a tap on it or the pencil, and saved with "Gem"; the sheet also removes it. A new item
765
+ // (+ in the top bar, "Tilføj punkt" under a submenu) opens its sheet at once; "Annuller" there takes it
766
+ // away again. The arrows move an item up and down.
767
+ export function MenuList({
768
+ field,
769
+ value,
770
+ onChange,
771
+ depth = 0,
772
+ }: {
773
+ field: Field;
774
+ value: unknown[];
775
+ onChange: (v: unknown) => void;
776
+ depth?: number;
777
+ }) {
778
+ const [open, setOpen] = useState<number | null>(null);
779
+ // The item whose sheet is open, its fields as they are being changed (and as they were, written as
780
+ // they are saved, to tell whether they have been changed), and whether it was just added.
781
+ const [editing, setEditing] = useState<{ i: number; value: Item; was: string; fresh: boolean } | null>(null);
782
+ const [problem, setProblem] = useState('');
783
+ // Why the sheet cannot be saved: a message in front of it and the field marked, as in every form.
784
+ const wrongField = useWrongField((text) => {
785
+ snackbar({ text, error: true });
786
+ setProblem(text);
787
+ });
788
+ const sheetRef = useRef<HTMLDialogElement>(null);
789
+ const list = useRef<HTMLUListElement>(null);
790
+ const length = useRef(value.length);
791
+ const sub = (field.fields ?? []).find((f) => f.type === 'list');
792
+ const own = (field.fields ?? []).filter((f) => f.type !== 'list');
793
+ const nameOf = (item: unknown) => summaryOf(field, item);
794
+ const singular = field.singular ?? t.forms.item;
795
+
796
+ const edit = (i: number, fresh = false) => {
797
+ setProblem('');
798
+ wrongField.stop(null);
799
+ const item = { ...((value[i] ?? {}) as Item) };
800
+ setEditing({ i, value: item, was: JSON.stringify(clean(own, item)), fresh });
801
+ requestAnimationFrame(() => sheetRef.current?.showModal());
802
+ };
803
+ const close = () => {
804
+ setEditing(null);
805
+ sheetRef.current?.close();
806
+ };
807
+ const save = () => {
808
+ if (!editing) return;
809
+ if (wrongField.stop(problemAt(own, editing.value))) return;
810
+ onChange(value.map((x, j) => (j === editing.i ? { ...(x as Item), ...editing.value } : x)));
811
+ close();
812
+ };
813
+ const cancel = () => {
814
+ if (editing?.fresh) onChange(value.filter((_, j) => j !== editing.i));
815
+ close();
816
+ };
817
+ // Closed (Annuller, the cross, a swipe down, Esc, a tap beside it) with the fields changed: the owner is
818
+ // asked whether to keep them ("Tilføj", "Gem"), leave them or stay.
819
+ const leave = async () => {
820
+ if (!editing || JSON.stringify(clean(own, editing.value)) === editing.was) return cancel();
821
+ const choice = await closeChanged(editing.fresh ? t.forms.add : t.common.save);
822
+ if (choice === 'keep') save();
823
+ else if (choice === 'discard') cancel();
824
+ };
825
+ const remove = async () => {
826
+ if (!editing) return;
827
+ const children = sub ? (((value[editing.i] as Item)?.[sub.name] ?? []) as unknown[]).length : 0;
828
+ const question = children ? t.forms.removeWithChildren(singular, children) : t.forms.removeQuestion(singular);
829
+ if (!(await ask(question, t.common.remove))) return;
830
+ onChange(value.filter((_, j) => j !== editing.i));
831
+ if (open === editing.i) setOpen(null);
832
+ close();
833
+ };
834
+ const move = (i: number, to: number) => {
835
+ const next = [...value];
836
+ [next[i], next[to]] = [next[to], next[i]];
837
+ onChange(next);
838
+ if (open === i) setOpen(to);
839
+ else if (open === to) setOpen(i);
840
+ };
841
+ // An item added at the end (+ in the top bar, or "Tilføj punkt") opens its sheet.
842
+ useEffect(() => {
843
+ if (value.length === length.current + 1) {
844
+ edit(value.length - 1, true);
845
+ requestAnimationFrame(() => list.current?.lastElementChild?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }));
846
+ }
847
+ length.current = value.length;
848
+ }, [value.length]);
849
+
850
+ return (
851
+ <div>
852
+ <ul ref={list} className={depth ? subListClass : listClass}>
853
+ {value.map((item, i) => {
854
+ const children = sub ? (((item as Item)?.[sub.name] ?? []) as unknown[]) : [];
855
+ return (
856
+ <ListItem
857
+ key={i}
858
+ tone={depth ? 'sub' : undefined}
859
+ onClick={() => edit(i)}
860
+ headline={nameOf(item) || t.forms.empty}
861
+ truncate
862
+ actions={
863
+ <span className="flex shrink-0 pr-1.5">
864
+ <IconButton icon={PencilSimple} label={t.forms.editOne(nameOf(item))} onClick={() => edit(i)} />
865
+ <IconButton icon={ArrowUp} label={t.common.moveUp} disabled={i === 0} onClick={() => move(i, i - 1)} />
866
+ <IconButton icon={ArrowDown} label={t.common.moveDown} disabled={i === value.length - 1} onClick={() => move(i, i + 1)} />
867
+ {sub && (
868
+ <IconButton
869
+ icon={CaretDown}
870
+ label={open === i ? t.forms.hideChildrenOf(nameOf(item)) : t.forms.showChildrenOf(nameOf(item))}
871
+ aria-expanded={open === i}
872
+ className={`[&>svg]:transition-transform [&>svg]:duration-(--motion-spatial-duration) [&>svg]:ease-(--motion-ease) ${open === i ? '[&>svg]:rotate-180' : ''}`}
873
+ onClick={() => setOpen(open === i ? null : i)}
874
+ />
875
+ )}
876
+ </span>
877
+ }
878
+ >
879
+ {sub && open === i && (
880
+ <div className="pb-2">
881
+ <MenuList
882
+ field={sub}
883
+ value={children}
884
+ onChange={(v) => onChange(value.map((x, j) => (j === i ? { ...(x as Item), [sub.name]: v } : x)))}
885
+ depth={depth + 1}
886
+ />
887
+ <Button
888
+ kind="text"
889
+ size="small"
890
+ icon={Plus}
891
+ className="mt-1 ml-5"
892
+ onClick={() =>
893
+ onChange(value.map((x, j) => (j === i ? { ...(x as Item), [sub.name]: [...children, newItem(sub)] } : x)))
894
+ }
895
+ >
896
+ {t.forms.addOne(sub.singular ?? t.forms.item)}
897
+ </Button>
898
+ </div>
899
+ )}
900
+ </ListItem>
901
+ );
902
+ })}
903
+ </ul>
904
+ <BottomSheet
905
+ ref={sheetRef}
906
+ aria-label={editing ? nameOf(editing.value) || capitalFirst(singular) : undefined}
907
+ onCancel={(e) => {
908
+ if (e.target !== e.currentTarget) return;
909
+ e.preventDefault();
910
+ void leave();
911
+ }}
912
+ onClick={(e) => e.target === sheetRef.current && void leave()}
913
+ >
914
+ {editing && (
915
+ <>
916
+ <SheetTitle title={nameOf(editing.value) || capitalFirst(singular)} onClose={() => void leave()} />
917
+ <SheetBody>
918
+ <PathHere.Provider value="">
919
+ <WrongField.Provider value={wrongField.field}>
920
+ <FieldList fields={own} value={editing.value} onChange={(v) => setEditing({ ...editing, value: v as Item })} />
921
+ </WrongField.Provider>
922
+ </PathHere.Provider>
923
+ {problem && <ProblemNote>{problem}</ProblemNote>}
924
+ </SheetBody>
925
+ <SheetActions
926
+ onCancel={() => void leave()}
927
+ onDone={save}
928
+ done={editing.fresh ? t.forms.add : t.common.save}
929
+ start={
930
+ !editing.fresh && (
931
+ <Button kind="text" icon={Trash} onClick={remove}>
932
+ {t.common.remove}
933
+ </Button>
934
+ )
935
+ }
936
+ />
937
+ </>
938
+ )}
939
+ </BottomSheet>
940
+ </div>
941
+ );
942
+ }