@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,1075 @@
1
+ // The text of a page or blog post: Milkdown (packages/editor/src/markdown.mjs) with a small toolbar.
2
+ // The text is shown in the site's own style (the "prose" classes of the pages). On pages the site's
3
+ // blocks (src/apps/<app>/blocks/) can be put in the text with a button; each is a card with the block as it looks
4
+ // on the site, which opens a menu (Ret, hide on the site, move, Fjern), with a free line kept between
5
+ // two cards to write in. A fixed page's sections are such a text too (SectionsField.tsx), so both look
6
+ // and work the same.
7
+ import { forwardRef, Fragment as ReactFragment, useEffect, useImperativeHandle, useRef, useState } from 'react';
8
+ import { editorViewCtx, type Editor } from '@milkdown/kit/core';
9
+ import type { CmdKey } from '@milkdown/kit/core';
10
+ import type { Ctx } from '@milkdown/kit/ctx';
11
+ import {
12
+ imageSchema,
13
+ insertImageCommand,
14
+ toggleEmphasisCommand,
15
+ toggleStrongCommand,
16
+ turnIntoTextCommand,
17
+ wrapInBlockquoteCommand,
18
+ wrapInBulletListCommand,
19
+ wrapInHeadingCommand,
20
+ wrapInOrderedListCommand,
21
+ } from '@milkdown/kit/preset/commonmark';
22
+ import { history, redoCommand, undoCommand } from '@milkdown/kit/plugin/history';
23
+ import { clipboard } from '@milkdown/kit/plugin/clipboard';
24
+ import { $prose, $view, callCommand } from '@milkdown/kit/utils';
25
+ import { Fragment } from '@milkdown/kit/prose/model';
26
+ import type { Mark, Node } from '@milkdown/kit/prose/model';
27
+ import { Decoration, DecorationSet, type EditorView } from '@milkdown/kit/prose/view';
28
+ import { NodeSelection, Plugin, Selection, TextSelection, type EditorState } from '@milkdown/kit/prose/state';
29
+ import { liftListItem } from '@milkdown/kit/prose/schema-list';
30
+ import { lift } from '@milkdown/kit/prose/commands';
31
+ import {
32
+ ArrowUUpLeft,
33
+ ArrowUUpRight,
34
+ Image,
35
+ LinkSimple,
36
+ ListBullets,
37
+ ListNumbers,
38
+ Plus,
39
+ Quotes,
40
+ TextAa,
41
+ TextB,
42
+ TextHThree,
43
+ TextHOne,
44
+ TextHTwo,
45
+ TextItalic,
46
+ TextT,
47
+ } from '@phosphor-icons/react';
48
+ import { HTML, makeEditor, markdownOf, partsOf, siteBlockSchema, spacing } from './markdown.mjs';
49
+ import { TEXT_COLORS, colorClass } from '../lib/text-colors.mjs';
50
+ import { IMAGE, type BlockAsk } from './BlockDialog';
51
+ import { askLink } from './LinkSheet';
52
+ import { Menu, MenuItem, Toolbar, ToolbarButton } from './ui';
53
+ import { shownAt } from './images';
54
+ import { cropStyle, joinPicture, splitPicture } from '../lib/crop.mjs';
55
+ import { describe, htmlBlock, type BlockProps } from './blocks';
56
+ import { preview } from './registry';
57
+ import { leaveTo } from './FormFrame';
58
+ import { createRoot } from 'react-dom/client';
59
+ import { flushSync } from 'react-dom';
60
+ import { t } from './i18n';
61
+ import { motionTiming } from '../lib/motion';
62
+ // The icons of the card's menu (Phosphor, as everywhere in the editor), as SVG: the card is not React.
63
+ import pencilIcon from '@phosphor-icons/core/assets/regular/pencil-simple.svg?raw';
64
+ import eyeIcon from '@phosphor-icons/core/assets/regular/eye.svg?raw';
65
+ import eyeSlashIcon from '@phosphor-icons/core/assets/regular/eye-slash.svg?raw';
66
+ import arrowUpIcon from '@phosphor-icons/core/assets/regular/arrow-up.svg?raw';
67
+ import arrowDownIcon from '@phosphor-icons/core/assets/regular/arrow-down.svg?raw';
68
+ import trashIcon from '@phosphor-icons/core/assets/regular/trash.svg?raw';
69
+ import boldIcon from '@phosphor-icons/core/assets/regular/text-b.svg?raw';
70
+ import italicIcon from '@phosphor-icons/core/assets/regular/text-italic.svg?raw';
71
+ import linkIcon from '@phosphor-icons/core/assets/regular/link-simple.svg?raw';
72
+ import h1Icon from '@phosphor-icons/core/assets/regular/text-h-one.svg?raw';
73
+ import h2Icon from '@phosphor-icons/core/assets/regular/text-h-two.svg?raw';
74
+ import h3Icon from '@phosphor-icons/core/assets/regular/text-h-three.svg?raw';
75
+ import paletteIcon from '@phosphor-icons/core/assets/regular/palette.svg?raw';
76
+ // ProseMirror's own styles: keep its invisible helpers (around pictures, at the end of lines) out of the layout.
77
+ import '@milkdown/kit/prose/view/style/prosemirror.css';
78
+
79
+ // parts: the text in parts, for a fixed page's sections (partsOf in markdown.mjs).
80
+ export type MarkdownHandle = { markdown: () => string | null; parts: () => ReturnType<typeof partsOf> | null };
81
+
82
+ // addImage: asks for a picture (the gallery, Media.tsx); its address on the site. Its description is
83
+ // asked in the sheet. Without it there is no picture button. blocks: asks which block to put in and its fields (BlockDialog);
84
+ // without it there is no block button; with it a free line is kept between two blocks, and before
85
+ // and after them (spacing in markdown.mjs). short: a smaller field, for a short text. h1: the page's own heading, "Overskrift
86
+ // (H1)", can be chosen too (a page's text, which has its H1 at the top; a blog post's is its title).
87
+ type Props = {
88
+ value: string;
89
+ onChange: () => void;
90
+ addImage?: () => Promise<string | null>;
91
+ blocks?: BlockAsk;
92
+ /** The sheet a picture or HTML is changed in, when there are no blocks (a blog post). */
93
+ sheet?: BlockAsk;
94
+ short?: boolean;
95
+ h1?: boolean;
96
+ /** How each part of the text looks on the site (looksOf in sections.mjs), shown so (editor.css). */
97
+ looks?: (items: LookItem[]) => (string | null)[];
98
+ /** The headings (H2) are the names of sections (a fixed page's, the front page's: sections.mjs), which the site
99
+ * draws as text: no colour for them. */
100
+ sections?: boolean;
101
+ };
102
+
103
+ // A part of the text at the top level, for `looks`.
104
+ export type LookItem = { kind: 'block'; type: string } | { kind: 'heading' | 'gap' | 'other' } | { kind: 'p'; image: boolean };
105
+
106
+ const lookItem = (node: Node): LookItem =>
107
+ node.type.name === 'siteBlock'
108
+ ? { kind: 'block', type: node.attrs.type }
109
+ : node.type.name === 'heading' && node.attrs.level === 2
110
+ ? { kind: 'heading' }
111
+ : node.type.name !== 'paragraph'
112
+ ? { kind: 'other' }
113
+ : node.content.size === 0
114
+ ? { kind: 'gap' }
115
+ : { kind: 'p', image: node.content.content.some((n) => n.type.name === 'image') };
116
+
117
+ // An empty paragraph: a free line between two blocks, which the arrows on a card skip.
118
+ const isGap = (node: Node) => node.type.name === 'paragraph' && node.content.size === 0;
119
+
120
+ // A card: a block, or a picture (in a paragraph of its own).
121
+ const isCard = (node: Node | null | undefined) => !!node && (node.type.name === 'siteBlock' || node.type.name === 'image');
122
+
123
+ // Whether deleting back (or forward) from the cursor reaches a card: one is selected, right before
124
+ // (after) the cursor, or before (after) the text the cursor is at the start (end) of.
125
+ function nextToCard(state: EditorState, back: boolean) {
126
+ const { selection } = state;
127
+ if (selection instanceof NodeSelection) return isCard(selection.node);
128
+ if (!selection.empty) return false;
129
+ const $pos = selection.$from;
130
+ if (isCard(back ? $pos.nodeBefore : $pos.nodeAfter)) return true;
131
+ if ($pos.parentOffset !== (back ? 0 : $pos.parent.content.size)) return false;
132
+ const next = $pos.node(-1).maybeChild($pos.index(-1) + (back ? -1 : 1));
133
+ return isCard(next) || isCard(back ? next?.lastChild : next?.firstChild);
134
+ }
135
+
136
+ // Deleting next to a card is ProseMirror's, as with a hardware keyboard (the card is selected, then
137
+ // removed). The keyboards of phones delete without a key (only beforeinput), and the browser then
138
+ // deleted the card, or what is in it, without ProseMirror knowing.
139
+ const cardDeletes = $prose(
140
+ () =>
141
+ new Plugin({
142
+ props: {
143
+ handleDOMEvents: {
144
+ beforeinput: (view, event) => {
145
+ const back = /^delete\w*Backward$/.test(event.inputType);
146
+ if (!back && !/^delete\w*Forward$/.test(event.inputType)) return false;
147
+ if (!nextToCard(view.state, back)) return false;
148
+ event.preventDefault();
149
+ const key = new KeyboardEvent('keydown', { key: back ? 'Backspace' : 'Delete', keyCode: back ? 8 : 46 });
150
+ view.someProp('handleKeyDown', (f) => f(view, key));
151
+ return true;
152
+ },
153
+ },
154
+ },
155
+ }),
156
+ );
157
+
158
+ // The free line the cursor is in, which opens up to write in (editor.css; the others are thin).
159
+ const gapHere = $prose(
160
+ () =>
161
+ new Plugin({
162
+ props: {
163
+ decorations: ({ doc, selection: { $from, empty } }) =>
164
+ empty && $from.depth === 1 && isGap($from.parent)
165
+ ? DecorationSet.create(doc, [Decoration.node($from.before(), $from.after(), { class: 'editor-gap-here' })])
166
+ : null,
167
+ },
168
+ }),
169
+ );
170
+
171
+ // The menu of the card that is open, if any: one at a time; closed by a tap elsewhere or Escape.
172
+ let closeOpenMenu: (() => void) | null = null;
173
+
174
+ // action: keyboard, whether the choice was made with the keyboard (Enter or Space).
175
+ type CardItem = { icon: string; label: string; action: (keyboard: boolean) => void; danger?: boolean };
176
+
177
+ // The part of the text at the top level before or after the one at index i (a block, or a paragraph
178
+ // with a picture), skipping the free lines between; null at the start or end.
179
+ function neighbour(doc: Node, i: number, by: -1 | 1) {
180
+ let j = i + by;
181
+ while (doc.maybeChild(j) && isGap(doc.child(j))) j += by;
182
+ return doc.maybeChild(j) ? j : null;
183
+ }
184
+
185
+ // The element of the part at the top level at index k.
186
+ const partDOM = (view: EditorView, k: number) => view.nodeDOM(view.state.doc.resolve(0).posAtIndex(k)) as HTMLElement | null;
187
+
188
+ // Swaps the part at the top level at index i with the one before or after it; free lines between
189
+ // stay. The parts glide from where they were to where they are now. Returns the moved part's new
190
+ // index, or null when it cannot move.
191
+ function move(view: EditorView, i: number, by: -1 | 1): number | null {
192
+ const { doc } = view.state;
193
+ const j = neighbour(doc, i, by);
194
+ if (j === null) return null;
195
+ const node = doc.child(i);
196
+ const [first, last] = by < 0 ? [j, i] : [i, j];
197
+ const nodes: Node[] = [];
198
+ for (let k = first; k <= last; k++) nodes.push(doc.child(k));
199
+ // Where each part is now, to let it glide from there; by the node, as the text may be tidied after
200
+ // the move (a section's heading goes with its card).
201
+ const tops = new Map<Node, number>();
202
+ doc.forEach((child, _, k) => {
203
+ const top = partDOM(view, k)?.getBoundingClientRect().top;
204
+ if (top !== undefined) tops.set(child, top);
205
+ });
206
+ const $start = doc.resolve(0);
207
+ const from = $start.posAtIndex(first);
208
+ const to = $start.posAtIndex(last) + doc.child(last).nodeSize;
209
+ const tr = view.state.tr.replaceWith(from, to, Fragment.from([nodes[nodes.length - 1], ...nodes.slice(1, -1), nodes[0]]));
210
+ // The cursor goes after the moved part, not on a block, which the next key would replace.
211
+ view.dispatch(tr.setSelection(Selection.near(tr.doc.resolve(by < 0 ? from + node.nodeSize : to))));
212
+ let at = -1;
213
+ view.state.doc.forEach((child, _, k) => {
214
+ if (child === node) at = k;
215
+ const was = tops.get(child);
216
+ const el = partDOM(view, k);
217
+ const dy = was === undefined || !el ? 0 : was - el.getBoundingClientRect().top;
218
+ if (el && Math.abs(dy) > 1 && !matchMedia('(prefers-reduced-motion: reduce)').matches)
219
+ el.animate([{ transform: `translateY(${dy}px)` }, { transform: 'none' }], motionTiming('spatial'));
220
+ });
221
+ if (at < 0) return null;
222
+ // Where it went, in view (one step: past one block, paragraph, heading or picture).
223
+ const moved = partDOM(view, at);
224
+ requestAnimationFrame(() => moved?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' }));
225
+ return at;
226
+ }
227
+
228
+ // Opens the menu of the card at the top level at index k again (after it moved, it is a new card).
229
+ type CardHost = HTMLElement & { openMenu?: (keyboard: boolean, focus?: string) => void };
230
+ function reopen(view: EditorView, k: number, keyboard: boolean, label: string) {
231
+ const el = partDOM(view, k);
232
+ const host = (el?.matches('.site-block') ? el : el?.querySelector('.site-block')) as CardHost | null;
233
+ host?.openMenu?.(keyboard, label);
234
+ }
235
+
236
+ // The blocks at the top level of the text, in order: their type and whether they are hidden.
237
+ const blocksOf = (doc: Node) => {
238
+ const blocks: { type: string; hidden: boolean }[] = [];
239
+ doc.forEach((n) => void (n.type.name === 'siteBlock' && blocks.push({ type: n.attrs.type, hidden: n.attrs.props?.hidden === true })));
240
+ return blocks;
241
+ };
242
+
243
+ // "Flyt op" and "Flyt ned" for the part at the top level at index i, where it can be moved: not past
244
+ // the start or end, nor past a block the template locks (locked: their types). The menu stays open on
245
+ // the moved card, so it can be moved again at once.
246
+ const moves = (view: EditorView, i: number | null, locked: string[] = []): CardItem[] => {
247
+ if (i === null) return [];
248
+ const step = (by: -1 | 1) => (keyboard: boolean) => {
249
+ const at = move(view, i, by);
250
+ if (at !== null) reopen(view, at, keyboard, by < 0 ? t.common.moveUp : t.common.moveDown);
251
+ };
252
+ const can = (by: -1 | 1) => {
253
+ const j = neighbour(view.state.doc, i, by);
254
+ const next = j === null ? null : view.state.doc.child(j);
255
+ return !!next && !(next.type.name === 'siteBlock' && locked.includes(next.attrs.type));
256
+ };
257
+ return [
258
+ ...(can(-1) ? [{ icon: arrowUpIcon, label: t.common.moveUp, action: step(-1) }] : []),
259
+ ...(can(1) ? [{ icon: arrowDownIcon, label: t.common.moveDown, action: step(1) }] : []),
260
+ ];
261
+ };
262
+
263
+ // The row of icons on a card (a block or a picture) in the text, opened by tapping it: items, as
264
+ // they are when it opens. Returns how to close it.
265
+ function cardMenu(view: EditorView, dom: HTMLElement, card: HTMLElement, name: string, items: () => (CardItem | undefined | '')[]) {
266
+ let menu: HTMLElement | null = null;
267
+ function close() {
268
+ menu?.remove();
269
+ menu = null;
270
+ card.setAttribute('aria-expanded', 'false');
271
+ document.removeEventListener('pointerdown', outside, true);
272
+ document.removeEventListener('keydown', escape, true);
273
+ if (closeOpenMenu === close) closeOpenMenu = null;
274
+ }
275
+ const outside = (e: Event) => void (dom.contains(e.target as globalThis.Node) || close());
276
+ const escape = (e: KeyboardEvent) => {
277
+ if (e.key !== 'Escape') return;
278
+ close();
279
+ card.focus();
280
+ };
281
+ // keyboard: opened with Enter or Space, so a choice gets the focus: the first, or the one named
282
+ // `focus` (the arrow used to move the card, when it opens again on the moved card).
283
+ function open(keyboard: boolean, focus?: string) {
284
+ closeOpenMenu?.();
285
+ menu = dom.appendChild(document.createElement('div'));
286
+ menu.className = 'site-block-menu';
287
+ menu.setAttribute('role', 'menu');
288
+ menu.setAttribute('aria-label', name);
289
+ for (const it of items()) {
290
+ if (!it) continue;
291
+ const b = menu.appendChild(document.createElement('button'));
292
+ b.type = 'button';
293
+ b.setAttribute('role', 'menuitem');
294
+ b.setAttribute('aria-label', it.label);
295
+ b.title = it.label;
296
+ b.innerHTML = it.icon.replace('<svg', '<svg width="20" height="20" fill="currentColor" aria-hidden="true"');
297
+ if (it.danger) b.className = 'site-block-danger';
298
+ b.addEventListener('click', (e) => {
299
+ close();
300
+ it.action(e.detail === 0);
301
+ });
302
+ }
303
+ card.setAttribute('aria-expanded', 'true');
304
+ closeOpenMenu = close;
305
+ document.addEventListener('pointerdown', outside, true);
306
+ document.addEventListener('keydown', escape, true);
307
+ // On a card taller than the screen, under the toolbar (not above the screen) where the card goes on.
308
+ const bar = document.querySelector('.editor-tools')?.getBoundingClientRect().bottom ?? 0;
309
+ const box = dom.getBoundingClientRect();
310
+ const at = menu.getBoundingClientRect();
311
+ if (at.top < bar + 8) menu.style.top = `${Math.min(bar + 8 - box.top, box.height - at.height - 8)}px`;
312
+ if (keyboard)
313
+ (menu.querySelector<HTMLElement>(`[aria-label="${focus}"]`) ?? menu.querySelector('button'))?.focus({ preventScroll: true });
314
+ }
315
+ (dom as CardHost).openMenu = open;
316
+ card.setAttribute('aria-haspopup', 'menu');
317
+ card.setAttribute('aria-expanded', 'false');
318
+ card.addEventListener('mousedown', (e) => e.preventDefault());
319
+ // While a finger is on the card, the text cannot be written in: a phone does not put the cursor in
320
+ // the text next to the card and open the keyboard (and a keyboard that is open closes).
321
+ card.addEventListener('pointerdown', (e) => {
322
+ if (e.pointerType === 'mouse' || view.props.editable?.(view.state) === false) return;
323
+ const before = view.props.editable;
324
+ view.setProps({ editable: () => false });
325
+ let timer = 0;
326
+ const unlock = () => {
327
+ clearTimeout(timer);
328
+ card.removeEventListener('click', later);
329
+ document.removeEventListener('pointercancel', unlock, true);
330
+ view.setProps({ editable: before });
331
+ };
332
+ const later = () => setTimeout(unlock);
333
+ card.addEventListener('click', later, { once: true });
334
+ document.addEventListener('pointercancel', unlock, { capture: true, once: true });
335
+ timer = window.setTimeout(unlock, 1000);
336
+ });
337
+ card.addEventListener('click', (e) => (menu ? close() : open(e.detail === 0)));
338
+ return close;
339
+ }
340
+
341
+ // The address of a picture from its sheet: with its format and crop after it, and marked when it is decorative
342
+ // (crop.mjs).
343
+ const pictureSrc = (props: BlockProps) =>
344
+ joinPicture(
345
+ String(props.src),
346
+ String(props.format ?? ''),
347
+ (props.crop as number[] | null | undefined) ?? null,
348
+ props.decorative === true,
349
+ );
350
+
351
+ // A picture in the text as a card like a block's, cut to its format, with a menu to change it
352
+ // (another picture, its description for screen readers, its format), move it with its paragraph and
353
+ // remove it.
354
+ // sheet: where it is changed (the sheet of the blocks, BlockDialog.tsx).
355
+ function pictureCard(node: Node, view: EditorView, getPos: () => number | undefined, sheet: () => BlockAsk | undefined) {
356
+ const dom = document.createElement('span');
357
+ dom.className = 'site-block site-picture';
358
+ dom.contentEditable = 'false';
359
+ const card = dom.appendChild(document.createElement('button'));
360
+ card.type = 'button';
361
+ card.className = 'site-block-card';
362
+ card.setAttribute('aria-label', t.text.image);
363
+ // Cut to its format as on the site (crop.mjs): in a box of the format.
364
+ const { src, format, crop, decorative } = splitPicture(node.attrs.src);
365
+ if (node.attrs.alt && !decorative) card.setAttribute('aria-description', node.attrs.alt);
366
+ const cut = format ? cropStyle(format, crop) : null;
367
+ const box = cut?.box ? card.appendChild(document.createElement('span')) : card;
368
+ if (cut?.box) {
369
+ box.className = 'picture-crop';
370
+ box.style.cssText = cut.box;
371
+ }
372
+ const img = box.appendChild(document.createElement('img'));
373
+ img.src = shownAt(src);
374
+ img.alt = '';
375
+ if (cut) img.style.cssText = cut.img;
376
+ if (node.attrs.title) img.title = node.attrs.title;
377
+ const describe = async () => {
378
+ const props = await sheet()?.edit(IMAGE, { src, alt: node.attrs.alt ?? '', format, crop, decorative });
379
+ const pos = getPos();
380
+ if (!props || pos === undefined) return;
381
+ view.dispatch(
382
+ view.state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, src: pictureSrc(props), alt: String(props.alt ?? '').trim() }),
383
+ );
384
+ };
385
+ const remove = () => {
386
+ const pos = getPos();
387
+ if (pos !== undefined) view.dispatch(view.state.tr.delete(pos, pos + node.nodeSize));
388
+ };
389
+ const close = cardMenu(view, dom, card, t.text.image, () => {
390
+ const pos = getPos();
391
+ return [
392
+ { icon: pencilIcon, label: t.common.edit, action: describe },
393
+ ...moves(view, pos === undefined ? null : view.state.doc.resolve(pos).index(0), sheet()?.template?.locked),
394
+ { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
395
+ ];
396
+ });
397
+ return { dom, update: () => false, stopEvent: () => true, ignoreMutation: () => true, destroy: close };
398
+ }
399
+
400
+ // The link where the cursor is, or in the selection: where it is (all of it, when the cursor is in it)
401
+ // and its address; null when there is none.
402
+ function linkAt(state: EditorView['state']) {
403
+ const type = state.schema.marks.link;
404
+ const { from, to, empty, $from } = state.selection;
405
+ if (!empty) {
406
+ let href = '';
407
+ state.doc.nodesBetween(from, to, (n) => void (href ||= type.isInSet(n.marks)?.attrs.href ?? ''));
408
+ return href ? { from, to, href } : null;
409
+ }
410
+ // The text with the same link around the cursor.
411
+ type Run = { from: number; to: number; mark: Mark };
412
+ const start = $from.start();
413
+ let run: Run | null = null;
414
+ let found = null as Run | null;
415
+ $from.parent.forEach((child, offset) => {
416
+ const mark = type.isInSet(child.marks);
417
+ const a = start + offset;
418
+ const b = a + child.nodeSize;
419
+ if (mark && run && run.mark.eq(mark) && run.to === a) run.to = b;
420
+ else run = mark ? { from: a, to: b, mark } : null;
421
+ if (run && run.from < from && from < run.to) found = { ...run };
422
+ });
423
+ return found && { from: found.from, to: found.to, href: String(found.mark.attrs.href) };
424
+ }
425
+
426
+ // The toolbar's and the selection's "Link": the link sheet (LinkSheet.tsx), then the selection, or
427
+ // the link the cursor is in, gets the address, or loses its link; with nothing selected, the page's
428
+ // name (or the address) is put in as the link.
429
+ async function editLink(view: EditorView) {
430
+ const link = linkAt(view.state);
431
+ const answer = await askLink(link?.href ?? '');
432
+ view.focus();
433
+ if (!answer) return;
434
+ const { state } = view;
435
+ const type = state.schema.marks.link;
436
+ const { from, to } = link ?? state.selection;
437
+ if (from === to) {
438
+ if (answer.href)
439
+ view.dispatch(state.tr.insertText(answer.text, from).addMark(from, from + answer.text.length, type.create({ href: answer.href })));
440
+ return;
441
+ }
442
+ const tr = state.tr.removeMark(from, to, type);
443
+ view.dispatch(answer.href ? tr.addMark(from, to, type.create({ href: answer.href })) : tr);
444
+ }
445
+
446
+ /** What applies where the cursor is, or to all of the selection: 'strong', 'emphasis', 'link',
447
+ * 'h2', 'h3', 'text' (a paragraph, not in a list or quote), 'bullet', 'ordered' and 'quote'; with a colour
448
+ * (text-colors.mjs) 'textColor' and the colour's class ('color-accent'). For the toolbar and the selection's menu,
449
+ * which mark them. */
450
+ export function activeOf(state: EditorView['state']): Set<string> {
451
+ const { selection, storedMarks, schema } = state;
452
+ const { $from, from, to, empty } = selection;
453
+ const on = new Set<string>();
454
+ for (const name of ['strong', 'emphasis', 'link', 'textColor']) {
455
+ const mark = schema.marks[name];
456
+ if (empty ? mark.isInSet(storedMarks ?? $from.marks()) : state.doc.rangeHasMark(from, to, mark)) on.add(name);
457
+ }
458
+ const color = colorAt(state);
459
+ if (color) on.add(colorClass(color));
460
+ const ancestors = Array.from({ length: $from.depth }, (_, i) => $from.node(i + 1).type.name);
461
+ if ($from.parent.type.name === 'heading') on.add(`h${$from.parent.attrs.level}`);
462
+ if (ancestors.includes('bullet_list')) on.add('bullet');
463
+ if (ancestors.includes('ordered_list')) on.add('ordered');
464
+ if (ancestors.includes('blockquote')) on.add('quote');
465
+ if ($from.parent.type.name === 'paragraph' && !on.has('bullet') && !on.has('ordered') && !on.has('quote')) on.add('text');
466
+ return on;
467
+ }
468
+
469
+ // The colour where the cursor is, or the first in the selection (text-colors.mjs); null without one.
470
+ function colorAt(state: EditorView['state']): string | null {
471
+ const type = state.schema.marks.textColor;
472
+ const { from, to, empty, $from } = state.selection;
473
+ if (empty) return type.isInSet(state.storedMarks ?? $from.marks())?.attrs.color ?? null;
474
+ let color: string | null = null;
475
+ state.doc.nodesBetween(from, to, (n) => void (color ??= type.isInSet(n.marks)?.attrs.color ?? null));
476
+ return color;
477
+ }
478
+
479
+ /** Gives the selection one of the site's colours (text-colors.mjs), or takes its colour away (null). */
480
+ export function paint(view: EditorView, color: string | null) {
481
+ const { state } = view;
482
+ const type = state.schema.marks.textColor;
483
+ const { from, to } = state.selection;
484
+ if (from === to) return view.focus();
485
+ const tr = state.tr.removeMark(from, to, type);
486
+ view.dispatch(color ? tr.addMark(from, to, type.create({ color })) : tr);
487
+ view.focus();
488
+ }
489
+
490
+ // off: not there for the selection (the button is hidden).
491
+ type Look = {
492
+ icon: string;
493
+ label: string;
494
+ on: (state: EditorView['state']) => boolean;
495
+ action: (button: HTMLElement) => void;
496
+ off?: (state: EditorView['state']) => boolean;
497
+ };
498
+
499
+ // Whether a heading (H2) is selected, or a part of one.
500
+ function inH2(state: EditorView['state']) {
501
+ let found = false;
502
+ const { from, to } = state.selection;
503
+ state.doc.nodesBetween(from, to, (n) => void (found ||= n.type.name === 'heading' && n.attrs.level === 2));
504
+ return found;
505
+ }
506
+
507
+ // When a word or more is selected: a row of icons over it, to make it bold, italic, a link, one of the site's colours
508
+ // (a menu under the button, opened by `colors`), the page's H1 (`h1`), a heading or a subheading; the ones that apply to
509
+ // the selection are marked. On a phone, where the phone's own menu is over the selection and its handles under it, the
510
+ // row is at the bottom of the screen, over the keyboard. sections: no colour in a heading (H2), the name of a section.
511
+ function selectionMenu(ctx: Ctx, h1: boolean, sections: boolean, colors: (button: HTMLElement) => void) {
512
+ return new Plugin({
513
+ view(view) {
514
+ const menu = document.body.appendChild(document.createElement('div'));
515
+ menu.className = 'editor-selection-menu';
516
+ menu.setAttribute('role', 'toolbar');
517
+ menu.setAttribute('aria-label', t.text.selection);
518
+ menu.hidden = true;
519
+ const run = <T,>(command: { key: CmdKey<T> }, payload?: T) => void callCommand(command.key, payload)(ctx);
520
+ const marked = (state: EditorView['state'], name: string) => activeOf(state).has(name);
521
+ const heading = (state: EditorView['state'], level: number) => activeOf(state).has(`h${level}`);
522
+ const looks: Look[] = [
523
+ { icon: boldIcon, label: t.text.bold, on: (s) => marked(s, 'strong'), action: () => run(toggleStrongCommand) },
524
+ { icon: italicIcon, label: t.text.italic, on: (s) => marked(s, 'emphasis'), action: () => run(toggleEmphasisCommand) },
525
+ {
526
+ icon: linkIcon,
527
+ label: t.text.link,
528
+ on: (s) => marked(s, 'link'),
529
+ action: () => void editLink(view).then(() => update(view)),
530
+ },
531
+ {
532
+ icon: paletteIcon,
533
+ label: t.text.color,
534
+ on: (s) => marked(s, 'textColor'),
535
+ action: colors,
536
+ off: sections ? inH2 : undefined,
537
+ },
538
+ ...(h1 ? [1, 2, 3] : [2, 3]).map((level) => ({
539
+ icon: [h1Icon, h2Icon, h3Icon][level - 1],
540
+ label: [t.text.pageHeading, t.text.heading, t.text.subheading][level - 1],
541
+ on: (s: EditorView['state']) => heading(s, level),
542
+ action: () => (heading(view.state, level) ? run(turnIntoTextCommand) : run(wrapInHeadingCommand, level)),
543
+ })),
544
+ ];
545
+ const buttons = looks.map((look) => {
546
+ const b = menu.appendChild(document.createElement('button'));
547
+ b.type = 'button';
548
+ b.title = look.label;
549
+ b.setAttribute('aria-label', look.label);
550
+ b.innerHTML = look.icon.replace('<svg', '<svg width="20" height="20" fill="currentColor" aria-hidden="true"');
551
+ if (look.action === colors) b.setAttribute('aria-haspopup', 'menu');
552
+ const act = () => {
553
+ look.action(b);
554
+ update(view);
555
+ // Again when "Gem ændringer" has come after the first change.
556
+ setTimeout(follow, 100);
557
+ };
558
+ // The selection stays: no focus taken with the mouse, and a tap does not end the selection
559
+ // (its click is the phone's, so the tap acts itself).
560
+ b.addEventListener('mousedown', (e) => e.preventDefault());
561
+ b.addEventListener('click', act);
562
+ b.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false });
563
+ b.addEventListener('touchend', (e) => {
564
+ e.preventDefault();
565
+ act();
566
+ });
567
+ return b;
568
+ });
569
+ function update(view: EditorView) {
570
+ const { selection, doc } = view.state;
571
+ const shown =
572
+ view.hasFocus() &&
573
+ selection instanceof TextSelection &&
574
+ !selection.empty &&
575
+ doc.textBetween(selection.from, selection.to).trim() !== '';
576
+ menu.hidden = !shown;
577
+ if (!shown) return;
578
+ looks.forEach((look, i) => {
579
+ buttons[i].hidden = !!look.off?.(view.state);
580
+ buttons[i].setAttribute('aria-pressed', String(look.on(view.state)));
581
+ });
582
+ const { offsetWidth: width, offsetHeight: height } = menu;
583
+ const screen = visualViewport ?? { offsetLeft: 0, offsetTop: 0, width: innerWidth, height: innerHeight };
584
+ if (matchMedia('(pointer: coarse)').matches) {
585
+ menu.style.left = `${screen.offsetLeft + (screen.width - width) / 2}px`;
586
+ // Over "Gem ændringer" when it is there (FormFrame.tsx).
587
+ const save = document.querySelector('.editor-save-bar')?.getBoundingClientRect().top ?? Infinity;
588
+ menu.style.top = `${Math.min(screen.offsetTop + screen.height, save) - height - 12}px`;
589
+ return;
590
+ }
591
+ const text = view.dom.getBoundingClientRect();
592
+ const start = view.coordsAtPos(selection.from);
593
+ const end = view.coordsAtPos(selection.to);
594
+ const middle = start.top === end.top ? (start.left + end.right) / 2 : text.left + text.width / 2;
595
+ // Over the selection; under it when the toolbar is there.
596
+ const bar = document.querySelector('.editor-tools')?.getBoundingClientRect().bottom ?? 0;
597
+ const over = start.top - height - 10;
598
+ menu.style.left = `${Math.max(4, Math.min(middle - width / 2, innerWidth - width - 4))}px`;
599
+ menu.style.top = `${over > bar ? over : end.bottom + 10}px`;
600
+ }
601
+ // Gone when the text is left (not for a tap on the menu itself, which keeps the focus).
602
+ const refresh = () => setTimeout(() => update(view));
603
+ // Where it is follows the page and the keyboard.
604
+ const follow = () => menu.hidden || update(view);
605
+ view.dom.addEventListener('blur', refresh);
606
+ view.dom.addEventListener('focus', refresh);
607
+ addEventListener('scroll', follow, { passive: true });
608
+ visualViewport?.addEventListener('resize', follow);
609
+ visualViewport?.addEventListener('scroll', follow);
610
+ return {
611
+ update,
612
+ destroy: () => {
613
+ view.dom.removeEventListener('blur', refresh);
614
+ view.dom.removeEventListener('focus', refresh);
615
+ removeEventListener('scroll', follow);
616
+ visualViewport?.removeEventListener('resize', follow);
617
+ visualViewport?.removeEventListener('scroll', follow);
618
+ menu.remove();
619
+ },
620
+ };
621
+ },
622
+ });
623
+ }
624
+
625
+ // A block in the text as a card: the block as it looks on the site (its draw, src/apps/<app>/blocks/), in a thin frame;
626
+ // for screen readers its name and what is in it (describe in blocks.ts). Tapping it opens a row of
627
+ // icons on it to edit, hide or show, move and remove it. cards: how to draw the previews of the
628
+ // text again, when what they show from elsewhere has changed.
629
+ function blockCard(
630
+ node: Node,
631
+ view: EditorView,
632
+ getPos: () => number | undefined,
633
+ asker: () => BlockAsk | undefined,
634
+ cards: Set<() => void>,
635
+ ) {
636
+ const ask = asker();
637
+ const block = node.attrs.type === HTML ? htmlBlock : ask?.list.find((b) => b.type === node.attrs.type);
638
+ const hidden = node.attrs.props.hidden === true;
639
+ // A block gone from the site by itself (a notice whose time is over) is greyed like a hidden one.
640
+ const over = !!block?.over?.(node.attrs.props);
641
+ const dom = document.createElement('div');
642
+ dom.className = hidden || over ? 'site-block site-block-hidden' : 'site-block';
643
+ dom.contentEditable = 'false';
644
+ const card = dom.appendChild(document.createElement('button'));
645
+ card.type = 'button';
646
+ card.className = 'site-block-card';
647
+ const name = block?.label ?? node.attrs.type;
648
+ card.setAttribute('aria-label', name);
649
+ if (hidden) {
650
+ // An eye struck through in the corner (for screen readers it says so in the description).
651
+ const badge = card.appendChild(document.createElement('span'));
652
+ badge.className = 'site-block-hidden-badge';
653
+ badge.innerHTML = eyeSlashIcon.replace('<svg', '<svg width="18" height="18" fill="currentColor" aria-hidden="true"');
654
+ }
655
+ const shown = card.appendChild(document.createElement('div'));
656
+ shown.className = ask?.data.page ? 'site-block-preview site-block-section not-prose' : 'site-block-preview not-prose';
657
+ shown.setAttribute('aria-hidden', 'true');
658
+ const root = createRoot(shown);
659
+ const draw = () => {
660
+ const ask = asker();
661
+ if (!ask) return;
662
+ const lines = block ? describe(block, node.attrs.props, ask.data) : [];
663
+ card.setAttribute('aria-description', [hidden ? t.text.hiddenOnPage : '', ...lines].filter(Boolean).join('. '));
664
+ // Without a picture of it, what is in it, in lines.
665
+ const picture = preview(node.attrs.type, node.attrs.props, ask.data) ?? (
666
+ <div className="site-block-lines">
667
+ {lines.map((line, i) => (
668
+ <span key={i}>{line}</span>
669
+ ))}
670
+ </div>
671
+ );
672
+ flushSync(() => root.render(picture));
673
+ };
674
+ draw();
675
+ cards.add(draw);
676
+
677
+ const at = () => {
678
+ const pos = getPos();
679
+ return pos === undefined ? null : { pos, $pos: view.state.doc.resolve(pos) };
680
+ };
681
+ // Where the card is at the top of the text, to move it.
682
+ const top = () => {
683
+ const pos = getPos();
684
+ return pos === undefined ? null : view.state.doc.resolve(pos).index(0);
685
+ };
686
+ const edit = async () => {
687
+ const props = await asker()?.edit(node.attrs.type, node.attrs.props);
688
+ const here = at();
689
+ // The block's fields as changed; still hidden if it was.
690
+ if (props && here)
691
+ view.dispatch(
692
+ view.state.tr.setNodeMarkup(here.pos, undefined, { ...node.attrs, props: hidden ? { ...props, hidden: true } : props }),
693
+ );
694
+ };
695
+ const toggle = () => {
696
+ const here = at();
697
+ if (!here) return;
698
+ const { hidden: _, ...props } = node.attrs.props;
699
+ view.dispatch(view.state.tr.setNodeMarkup(here.pos, undefined, { ...node.attrs, props: hidden ? props : { ...props, hidden: true } }));
700
+ };
701
+ const remove = () => {
702
+ const here = at();
703
+ if (here) view.dispatch(view.state.tr.delete(here.pos, here.pos + node.nodeSize));
704
+ };
705
+
706
+ // The choices that can be made here: no "Ret" without fields (but to where it is edited instead,
707
+ // Block.edit), no move past the start or end. By the template (collections.ts): a locked block is
708
+ // only edited, and the last shown one of a block the page must have cannot be hidden or removed.
709
+ const close = cardMenu(view, dom, card, name, () => {
710
+ const i = top();
711
+ const rules = asker()?.template;
712
+ const type = node.attrs.type;
713
+ const locked = !!rules?.locked?.includes(type);
714
+ const shownOfType = blocksOf(view.state.doc).filter((b) => b.type === type && b.hidden === false).length;
715
+ // (Not written with "!" before "hidden", which Tailwind would take for a class for the site.)
716
+ const last = hidden === false && !!rules?.required?.includes(type) && shownOfType <= 1;
717
+ const stays = locked || last;
718
+ return [
719
+ block?.fields.length
720
+ ? { icon: pencilIcon, label: t.common.edit, action: edit }
721
+ : block?.edit && { icon: pencilIcon, label: block.edit.text, action: () => leaveTo(`#${block.edit!.to}`) },
722
+ block?.shown || (stays && hidden === false)
723
+ ? undefined
724
+ : { icon: hidden ? eyeIcon : eyeSlashIcon, label: hidden ? t.text.showOnPage : t.text.hideOnPage, action: toggle },
725
+ ...(locked ? [] : moves(view, i, rules?.locked)),
726
+ stays ? undefined : { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
727
+ ];
728
+ });
729
+ // A changed block is drawn anew (update false); taps on the card and its menu are not ProseMirror's.
730
+ return {
731
+ dom,
732
+ update: () => false,
733
+ stopEvent: () => true,
734
+ ignoreMutation: () => true,
735
+ destroy: () => {
736
+ close();
737
+ cards.delete(draw);
738
+ // Not while React may be drawing (the editor goes with its form).
739
+ queueMicrotask(() => root.unmount());
740
+ },
741
+ };
742
+ }
743
+
744
+ export default forwardRef<MarkdownHandle, Props>(function MarkdownEditor(
745
+ { value, onChange, addImage, blocks, sheet: sheetProp, short, h1, looks, sections },
746
+ ref,
747
+ ) {
748
+ const spaced = !!blocks;
749
+ const root = useRef<HTMLDivElement>(null);
750
+ const toolbar = useRef<HTMLDivElement>(null);
751
+ const editor = useRef<Editor | null>(null);
752
+ const changed = useRef(onChange);
753
+ changed.current = onChange;
754
+ const sheeted = useRef(blocks ?? sheetProp);
755
+ sheeted.current = blocks ?? sheetProp;
756
+ const looked = useRef(looks);
757
+ looked.current = looks;
758
+ // What applies where the cursor is (activeOf), marked in the toolbar.
759
+ const [active, setActive] = useState<string[]>([]);
760
+ // The colour button over the selection, while its menu with the colours is open.
761
+ const [colorsAt, setColorsAt] = useState<HTMLElement | null>(null);
762
+ useEffect(() => {
763
+ colorsAt?.setAttribute('aria-expanded', 'true');
764
+ return () => colorsAt?.setAttribute('aria-expanded', 'false');
765
+ }, [colorsAt]);
766
+ // The previews of the blocks, drawn again when what they show from elsewhere changes: the lists,
767
+ // the testimonials, the rest of the site, the page's address or a fixed page's other fields.
768
+ const cards = useRef(new Set<() => void>());
769
+ const data = blocks?.data;
770
+ useEffect(
771
+ () => queueMicrotask(() => cards.current.forEach((draw) => draw())),
772
+ [data?.lists, data?.testimonials, data?.site, data?.path, data?.page?.values],
773
+ );
774
+
775
+ useEffect(() => {
776
+ let cancelled = false;
777
+ // Tell the form as soon as the text changes (Milkdown's own listener waits 200 ms); not for the
778
+ // free lines put in as the text is loaded.
779
+ let loaded = false;
780
+ const marking = $prose(
781
+ () =>
782
+ new Plugin({
783
+ view: () => ({
784
+ update: (view) => {
785
+ const now = [...activeOf(view.state)].sort();
786
+ setActive((was) => (was.join() === now.join() ? was : now));
787
+ },
788
+ }),
789
+ }),
790
+ );
791
+ const onEdit = $prose(
792
+ () => new Plugin({ view: () => ({ update: (view, prev) => void (!loaded || view.state.doc.eq(prev.doc) || changed.current()) }) }),
793
+ );
794
+ const selecting = $prose((ctx) => selectionMenu(ctx, !!h1, !!sections, (button) => setColorsAt((open) => (open ? null : button))));
795
+ const pictureView = $view(imageSchema.node, () => (node, view, getPos) => pictureCard(node, view, getPos, () => sheeted.current));
796
+ const cardView = $view(
797
+ siteBlockSchema.node,
798
+ () => (node, view, getPos) => blockCard(node, view, getPos, () => sheeted.current, cards.current),
799
+ );
800
+ // The look of each part on the site, as data-look on it.
801
+ const looking = $prose(
802
+ () =>
803
+ new Plugin({
804
+ props: {
805
+ decorations: (state) => {
806
+ const looks = looked.current;
807
+ if (!looks) return null;
808
+ const nodes: { node: Node; pos: number }[] = [];
809
+ state.doc.forEach((node, pos) => void nodes.push({ node, pos }));
810
+ const marks = looks(nodes.map(({ node }) => lookItem(node)));
811
+ return DecorationSet.create(
812
+ state.doc,
813
+ nodes.flatMap(({ node, pos }, i) =>
814
+ marks[i] ? [Decoration.node(pos, pos + node.nodeSize, { 'data-look': marks[i]! })] : [],
815
+ ),
816
+ );
817
+ },
818
+ },
819
+ }),
820
+ );
821
+ const made = makeEditor(root.current, value)
822
+ .use(cardView)
823
+ .use(pictureView)
824
+ .use(selecting)
825
+ .use(cardDeletes)
826
+ .use(marking)
827
+ .use(onEdit)
828
+ .use(looking)
829
+ .use(gapHere)
830
+ .use(history)
831
+ .use(clipboard);
832
+ (spaced ? made.use(spacing) : made).create().then((created) => {
833
+ if (cancelled) return created.destroy();
834
+ editor.current = created;
835
+ // The free lines between the blocks of the text as it is loaded, and the cursor in the first,
836
+ // so a key pressed before the text is tapped does not replace the first block.
837
+ if (spaced)
838
+ created.action((ctx) => {
839
+ const view = ctx.get(editorViewCtx);
840
+ view.dispatch(view.state.tr.setMeta('addToHistory', false));
841
+ view.dispatch(view.state.tr.setSelection(Selection.atStart(view.state.doc)).setMeta('addToHistory', false));
842
+ });
843
+ loaded = true;
844
+ });
845
+ return () => {
846
+ cancelled = true;
847
+ editor.current?.destroy();
848
+ editor.current = null;
849
+ };
850
+ // The editor is made once per file; the parent gives each file its own key.
851
+ }, []);
852
+
853
+ useImperativeHandle(ref, () => ({
854
+ markdown: () => editor.current?.action(markdownOf) ?? null,
855
+ parts: () => editor.current?.action(partsOf) ?? null,
856
+ }));
857
+
858
+ function run<T>(command: { key: CmdKey<T> }, payload?: T) {
859
+ editor.current?.action(callCommand(command.key, payload));
860
+ }
861
+
862
+ // Makes the text where the cursor is a heading ('h1', 'h2', 'h3'), a paragraph ('text'), a list
863
+ // ('bullet', 'ordered') or a quote ('quote'), whatever it is now: a list becomes the other kind of
864
+ // list, and the text of a list or a quote comes out of it to be something else.
865
+ function setKind(kind: string) {
866
+ editor.current?.action((ctx) => {
867
+ const view = ctx.get(editorViewCtx);
868
+ const { schema, selection } = view.state;
869
+ const is = (k: string) => activeOf(view.state).has(k);
870
+ const outOfLists = () => {
871
+ for (let i = 0; i < 10 && (is('bullet') || is('ordered')) && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++);
872
+ };
873
+ const outOfQuotes = () => {
874
+ for (let i = 0; i < 10 && is('quote') && lift(view.state, view.dispatch); i++);
875
+ };
876
+ if (kind === 'quote') {
877
+ if (!is('quote')) {
878
+ outOfLists();
879
+ if (is('h1') || is('h2') || is('h3')) callCommand(turnIntoTextCommand.key)(ctx);
880
+ callCommand(wrapInBlockquoteCommand.key)(ctx);
881
+ }
882
+ return view.focus();
883
+ }
884
+ if (kind === 'bullet' || kind === 'ordered') {
885
+ const list = schema.nodes[kind === 'bullet' ? 'bullet_list' : 'ordered_list'];
886
+ const { $from } = selection;
887
+ for (let d = $from.depth; d > 0; d--) {
888
+ const node = $from.node(d);
889
+ if (node.type.name !== 'bullet_list' && node.type.name !== 'ordered_list') continue;
890
+ if (node.type !== list) view.dispatch(view.state.tr.setNodeMarkup($from.before(d), list, { spread: node.attrs.spread }));
891
+ return view.focus();
892
+ }
893
+ outOfQuotes();
894
+ if (is('h1') || is('h2') || is('h3')) callCommand(turnIntoTextCommand.key)(ctx);
895
+ callCommand((kind === 'bullet' ? wrapInBulletListCommand : wrapInOrderedListCommand).key)(ctx);
896
+ return view.focus();
897
+ }
898
+ outOfLists();
899
+ outOfQuotes();
900
+ if (kind === 'text') callCommand(turnIntoTextCommand.key)(ctx);
901
+ else callCommand(wrapInHeadingCommand.key, Number(kind.slice(1)))(ctx);
902
+ view.focus();
903
+ });
904
+ }
905
+
906
+ function link() {
907
+ editor.current?.action((ctx) => void editLink(ctx.get(editorViewCtx)));
908
+ }
909
+
910
+ function color(name: string | null) {
911
+ editor.current?.action((ctx) => paint(ctx.get(editorViewCtx), name));
912
+ }
913
+
914
+ // A new picture from the gallery, then the sheet with it and what it shows, where it can still be
915
+ // changed or left out (the browser's own box for what it shows without a sheet).
916
+ async function image() {
917
+ const src = await addImage!();
918
+ if (!src) return;
919
+ const sheet = blocks ?? sheetProp;
920
+ const picked = await sheet?.edit(IMAGE, { src, alt: '' }, true);
921
+ if (picked) run(insertImageCommand, { src: pictureSrc(picked), alt: String(picked.alt ?? '').trim() });
922
+ }
923
+
924
+ // Puts the chosen block where the cursor is (a paragraph is split around it), or after the block
925
+ // that is selected. With no text after it, an empty paragraph follows, where the cursor goes, so
926
+ // the next text or block comes after it instead of replacing it.
927
+ async function block() {
928
+ // The blocks and pictures in the text now, for how many of a type there may be (the template).
929
+ let have: string[] = [];
930
+ editor.current?.action((ctx) => {
931
+ const doc = ctx.get(editorViewCtx).state.doc;
932
+ have = blocksOf(doc).map((b) => b.type);
933
+ doc.descendants((n) => void (n.type.name === 'image' && have.push(IMAGE)));
934
+ });
935
+ const chosen = await blocks!.choose({ image: !!addImage, have });
936
+ if (!chosen) return;
937
+ if (chosen.type === IMAGE) return image();
938
+ editor.current?.action((ctx) => {
939
+ const view = ctx.get(editorViewCtx);
940
+ const { state } = view;
941
+ const node = siteBlockSchema.type(ctx).create({ type: chosen.type, props: chosen.props });
942
+ let tr = state.tr;
943
+ // Where the new block ends, when the cursor must be moved past it (-1: it is in text already).
944
+ let end = -1;
945
+ if (state.selection instanceof NodeSelection) {
946
+ tr = tr.insert(state.selection.to, node);
947
+ end = state.selection.to + node.nodeSize;
948
+ } else {
949
+ tr = tr.replaceSelectionWith(node);
950
+ if (tr.selection instanceof NodeSelection) end = tr.selection.to;
951
+ }
952
+ if (end >= 0) {
953
+ if (!tr.doc.resolve(end).nodeAfter?.isTextblock) tr = tr.insert(end, state.schema.nodes.paragraph.create());
954
+ tr = tr.setSelection(TextSelection.create(tr.doc, end + 1));
955
+ }
956
+ // A section whose heading is written above its card gets one, its name, selected to write over.
957
+ const chosenBlock = blocks!.list.find((b) => b.type === chosen.type);
958
+ if (chosenBlock?.lead?.heading) {
959
+ let at = -1;
960
+ tr.doc.forEach((n, offset) => void (n === node && (at = offset)));
961
+ if (at >= 0) {
962
+ tr = tr.insert(at, state.schema.nodes.heading.create({ level: 2 }, state.schema.text(chosenBlock.label)));
963
+ tr = tr.setSelection(TextSelection.create(tr.doc, at + 1, at + 1 + chosenBlock.label.length));
964
+ }
965
+ }
966
+ view.dispatch(tr.scrollIntoView());
967
+ view.focus();
968
+ });
969
+ }
970
+
971
+ const tools: { label: string; icon: typeof TextB; action: () => void; on?: string }[] = [
972
+ { label: t.text.bold, icon: TextB, action: () => run(toggleStrongCommand), on: 'strong' },
973
+ { label: t.text.italic, icon: TextItalic, action: () => run(toggleEmphasisCommand), on: 'emphasis' },
974
+ { label: t.text.link, icon: LinkSimple, action: link, on: 'link' },
975
+ ...(h1 ? [{ label: t.text.pageHeading, icon: TextHOne, action: () => setKind('h1'), on: 'h1' }] : []),
976
+ { label: t.text.heading, icon: TextHTwo, action: () => setKind('h2'), on: 'h2' },
977
+ { label: t.text.subheading, icon: TextHThree, action: () => setKind('h3'), on: 'h3' },
978
+ { label: t.text.paragraph, icon: TextT, action: () => setKind('text'), on: 'text' },
979
+ { label: t.text.bulletList, icon: ListBullets, action: () => setKind('bullet'), on: 'bullet' },
980
+ { label: t.text.orderedList, icon: ListNumbers, action: () => setKind('ordered'), on: 'ordered' },
981
+ { label: t.text.quote, icon: Quotes, action: () => setKind('quote'), on: 'quote' },
982
+ // A picture and the blocks are put in with "+" (a sheet with them all); without blocks (a blog
983
+ // post) a picture has its own button.
984
+ ...(blocks
985
+ ? [{ label: t.text.insert, icon: Plus, action: block }]
986
+ : addImage
987
+ ? [{ label: t.text.image, icon: Image, action: image }]
988
+ : []),
989
+ { label: t.text.undo, icon: ArrowUUpLeft, action: () => run(undoCommand) },
990
+ { label: t.text.redo, icon: ArrowUUpRight, action: () => run(redoCommand) },
991
+ ];
992
+
993
+ // When the toolbar is too short for all its buttons, the kinds of text (H1, H2, H3, T, the bulleted and
994
+ // the numbered list and the quote) are one button: the one where the cursor is, with an arrow, and
995
+ // the others under it.
996
+ const kinds = tools.filter((t) => ['h1', 'h2', 'h3', 'text', 'bullet', 'ordered', 'quote'].includes(t.on ?? ''));
997
+ const kind = kinds.find((t) => active.includes(t.on!));
998
+ const shown = kind ?? kinds.find((t) => t.on === 'text')!;
999
+ const [narrow, setNarrow] = useState(false);
1000
+ // The button that opened the menu with the kinds of text, while it is open.
1001
+ const [kindsAt, setKindsAt] = useState<HTMLElement | null>(null);
1002
+ useEffect(() => {
1003
+ const bar = toolbar.current!;
1004
+ // A button is 2.5rem, with 0.125rem between them and 0.375rem around them, and each of the lines
1005
+ // between the groups 0.5rem (the classes below).
1006
+ const fits = () => setNarrow(bar.clientWidth < tools.length * 42 + 10 + 3 * 10);
1007
+ const watch = new ResizeObserver(fits);
1008
+ watch.observe(bar);
1009
+ return () => watch.disconnect();
1010
+ }, [tools.length]);
1011
+ const tool = ({ label, icon, action, on }: (typeof tools)[number]) => (
1012
+ <ToolbarButton key={label} icon={icon} label={label} selected={on ? active.includes(on) : undefined} onClick={action} />
1013
+ );
1014
+
1015
+ // The lines between the marks on the text, the kinds of text, "+" (or "Billede") and "Fortryd".
1016
+ const lineBefore = (item: (typeof tools)[number]) =>
1017
+ item === kinds[0] || item.icon === Plus || item.icon === Image || item.icon === ArrowUUpLeft;
1018
+ // A button of the toolbar; when it is too short, the kinds of text are one button (at the first), with a menu.
1019
+ const button = (item: (typeof tools)[number]) =>
1020
+ !narrow || !kinds.includes(item) ? (
1021
+ tool(item)
1022
+ ) : item === kinds[0] ? (
1023
+ // Not marked: it always shows the kind there is (the menu under it marks it).
1024
+ <ToolbarButton
1025
+ key="kinds"
1026
+ icon={shown.icon}
1027
+ label={t.text.kindIs(shown.label)}
1028
+ menu={!!kindsAt}
1029
+ onClick={(e) => setKindsAt(kindsAt ? null : e.currentTarget)}
1030
+ />
1031
+ ) : null;
1032
+
1033
+ return (
1034
+ <div className="rounded-panel border border-line bg-surface">
1035
+ {/* Stays at the top while scrolling through the text, a little under the app's top bar (Screen.tsx,
1036
+ 4rem and its line); on a phone one row that scrolls sideways. The buttons are centred, but
1037
+ start at the left edge when they do not fit (safe), so none is out of reach. */}
1038
+ <Toolbar
1039
+ ref={toolbar}
1040
+ label={t.text.tools}
1041
+ className="editor-tools sticky top-[calc(4.5rem+1px+env(safe-area-inset-top))] z-10 justify-center-safe overflow-x-auto rounded-t-panel border-b border-line md:flex-wrap"
1042
+ >
1043
+ {tools.map((tool) => (
1044
+ <ReactFragment key={tool.label}>
1045
+ {lineBefore(tool) && <span aria-hidden="true" className="mx-1 my-2 w-px shrink-0 self-stretch bg-line" />}
1046
+ {button(tool)}
1047
+ </ReactFragment>
1048
+ ))}
1049
+ </Toolbar>
1050
+ {/* The site's colours (text-colors.mjs), each named in its colour, as it is in the text (its class), and "Ingen
1051
+ farve" in the text's own. */}
1052
+ <Menu anchor={colorsAt} label={t.text.color} onClose={() => setColorsAt(null)}>
1053
+ {TEXT_COLORS.map((name) => (
1054
+ <MenuItem key={name} icon={TextAa} selected={active.includes(colorClass(name))} onClick={() => color(name)}>
1055
+ <span className={colorClass(name)}>{t.text.colors[name as keyof typeof t.text.colors]}</span>
1056
+ </MenuItem>
1057
+ ))}
1058
+ <MenuItem icon={TextAa} selected={!active.includes('textColor')} onClick={() => color(null)}>
1059
+ <span className="text-ink">{t.text.noColor}</span>
1060
+ </MenuItem>
1061
+ </Menu>
1062
+ <Menu anchor={kindsAt} label={t.text.kind} onClose={() => setKindsAt(null)}>
1063
+ {kinds.map(({ label, icon, action, on }) => (
1064
+ <MenuItem key={label} icon={icon} selected={active.includes(on!)} onClick={action}>
1065
+ {label}
1066
+ </MenuItem>
1067
+ ))}
1068
+ </Menu>
1069
+ <div
1070
+ ref={root}
1071
+ className={`editor-text prose max-w-none px-5 py-4 md:px-8 ${short ? 'editor-text-short' : ''} ${spaced ? 'editor-spaced' : ''}`}
1072
+ />
1073
+ </div>
1074
+ );
1075
+ });