@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (908) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +51 -51
  4. package/astro.mjs +162 -162
  5. package/bin/morpheme.mjs +38 -38
  6. package/client.d.ts +10 -10
  7. package/defaults/blockTexts.ts +33 -33
  8. package/defaults/collections.ts +774 -774
  9. package/defaults/colors.ts +125 -125
  10. package/defaults/company.ts +20 -20
  11. package/defaults/editor.ts +178 -178
  12. package/defaults/internalLinks.ts +30 -30
  13. package/defaults/opens.ts +18 -18
  14. package/defaults/popup.tsx +93 -93
  15. package/defaults/privacy.ts +79 -79
  16. package/defaults/rules.ts +450 -450
  17. package/defaults/schema.ts +16 -16
  18. package/defaults/sitedata.ts +129 -129
  19. package/defaults/standard.ts +106 -106
  20. package/defaults/standardPages.ts +87 -87
  21. package/defaults/texts.ts +481 -481
  22. package/demo/README.md +26 -26
  23. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
  24. package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
  25. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
  26. package/demo/src/content/blog/every-save-is-a-version.md +21 -21
  27. package/demo/src/content/blog/schedule-a-post.md +17 -17
  28. package/demo/src/content/blog/secure-from-day-one.md +17 -17
  29. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
  30. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
  31. package/demo/src/content/blog/the-app-store.md +21 -21
  32. package/demo/src/content/pages/about.md +22 -22
  33. package/demo/src/content/pages/app-store.md +30 -30
  34. package/demo/src/content/pages/blocks.md +98 -98
  35. package/demo/src/content/pages/blog-page.md +11 -11
  36. package/demo/src/content/pages/contact.md +16 -16
  37. package/demo/src/content/pages/get-started.md +20 -20
  38. package/demo/src/content/pages/home.md +26 -26
  39. package/demo/src/content/pages/page-not-found.md +11 -11
  40. package/demo/src/content/pages/privacy-policy.md +24 -24
  41. package/demo/src/data/blocks.json +25 -25
  42. package/demo/src/data/booking.json +6 -6
  43. package/demo/src/data/categories/apps.json +6 -6
  44. package/demo/src/data/categories/design.json +6 -6
  45. package/demo/src/data/categories/editing.json +6 -6
  46. package/demo/src/data/categories/security.json +6 -6
  47. package/demo/src/data/categories/seo.json +6 -6
  48. package/demo/src/data/colors.json +75 -75
  49. package/demo/src/data/contact.json +8 -8
  50. package/demo/src/data/cookies.json +21 -21
  51. package/demo/src/data/footer.json +27 -27
  52. package/demo/src/data/menu.json +35 -35
  53. package/demo/src/data/mobile-menu.json +25 -25
  54. package/demo/src/data/popups/booking-bar.json +13 -13
  55. package/demo/src/data/popups/try-the-editor.json +16 -16
  56. package/demo/src/data/standard-pages.json +8 -8
  57. package/demo/src/data/testimonials/sample-bakery.json +10 -10
  58. package/demo/src/data/testimonials/sample-clinic.json +10 -10
  59. package/demo/src/data/testimonials/sample-studio.json +10 -10
  60. package/demo/src/site/colors.ts +11 -11
  61. package/demo/src/site/config.ts +19 -19
  62. package/icons/lucide/LICENSE +43 -43
  63. package/icons/lucide/address-book.svg +1 -1
  64. package/icons/lucide/app-window.svg +1 -1
  65. package/icons/lucide/arrow-counter-clockwise.svg +1 -1
  66. package/icons/lucide/arrow-down.svg +1 -1
  67. package/icons/lucide/arrow-left.svg +1 -1
  68. package/icons/lucide/arrow-right.svg +1 -1
  69. package/icons/lucide/arrow-square-out.svg +1 -1
  70. package/icons/lucide/arrow-u-up-left.svg +1 -1
  71. package/icons/lucide/arrow-u-up-right.svg +1 -1
  72. package/icons/lucide/arrow-up-right.svg +1 -1
  73. package/icons/lucide/arrow-up.svg +1 -1
  74. package/icons/lucide/arrows-down-up.svg +1 -1
  75. package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
  76. package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
  77. package/icons/lucide/article.svg +1 -1
  78. package/icons/lucide/bank.svg +1 -1
  79. package/icons/lucide/bell.svg +1 -1
  80. package/icons/lucide/bicycle.svg +1 -1
  81. package/icons/lucide/binoculars.svg +1 -1
  82. package/icons/lucide/book-open-text.svg +1 -1
  83. package/icons/lucide/book-open.svg +1 -1
  84. package/icons/lucide/browser.svg +1 -1
  85. package/icons/lucide/buildings.svg +1 -1
  86. package/icons/lucide/bus.svg +1 -1
  87. package/icons/lucide/calendar-blank.svg +1 -1
  88. package/icons/lucide/calendar-check.svg +1 -1
  89. package/icons/lucide/car.svg +1 -1
  90. package/icons/lucide/cards.svg +1 -1
  91. package/icons/lucide/caret-down.svg +1 -1
  92. package/icons/lucide/caret-left.svg +1 -1
  93. package/icons/lucide/caret-right.svg +1 -1
  94. package/icons/lucide/caret-up.svg +1 -1
  95. package/icons/lucide/certificate.svg +1 -1
  96. package/icons/lucide/chart-line-up.svg +1 -1
  97. package/icons/lucide/chats.svg +1 -1
  98. package/icons/lucide/check-circle.svg +1 -1
  99. package/icons/lucide/check.svg +1 -1
  100. package/icons/lucide/circle-half.svg +1 -1
  101. package/icons/lucide/circle.svg +1 -1
  102. package/icons/lucide/clock-counter-clockwise.svg +1 -1
  103. package/icons/lucide/clock.svg +1 -1
  104. package/icons/lucide/code.svg +1 -1
  105. package/icons/lucide/coffee.svg +1 -1
  106. package/icons/lucide/columns.svg +1 -1
  107. package/icons/lucide/cookie.svg +1 -1
  108. package/icons/lucide/copy.svg +1 -1
  109. package/icons/lucide/credit-card.svg +1 -1
  110. package/icons/lucide/currency-circle-dollar.svg +1 -1
  111. package/icons/lucide/cursor-click.svg +1 -1
  112. package/icons/lucide/database.svg +1 -1
  113. package/icons/lucide/desktop.svg +1 -1
  114. package/icons/lucide/device-mobile.svg +1 -1
  115. package/icons/lucide/door-open.svg +1 -1
  116. package/icons/lucide/download-simple.svg +1 -1
  117. package/icons/lucide/envelope-simple.svg +1 -1
  118. package/icons/lucide/eye-slash.svg +1 -1
  119. package/icons/lucide/eye.svg +1 -1
  120. package/icons/lucide/file-pdf.svg +1 -1
  121. package/icons/lucide/file-text.svg +1 -1
  122. package/icons/lucide/file-video.svg +1 -1
  123. package/icons/lucide/file-zip.svg +1 -1
  124. package/icons/lucide/flower-lotus.svg +1 -1
  125. package/icons/lucide/folder-simple.svg +1 -1
  126. package/icons/lucide/funnel.svg +1 -1
  127. package/icons/lucide/gauge.svg +1 -1
  128. package/icons/lucide/gear.svg +1 -1
  129. package/icons/lucide/github-logo.svg +1 -1
  130. package/icons/lucide/globe.svg +1 -1
  131. package/icons/lucide/google-logo.svg +1 -1
  132. package/icons/lucide/graduation-cap.svg +1 -1
  133. package/icons/lucide/hammer.svg +1 -1
  134. package/icons/lucide/hand-heart.svg +1 -1
  135. package/icons/lucide/handshake.svg +1 -1
  136. package/icons/lucide/hard-drives.svg +1 -1
  137. package/icons/lucide/heart.svg +1 -1
  138. package/icons/lucide/house.svg +1 -1
  139. package/icons/lucide/image-square.svg +1 -1
  140. package/icons/lucide/image.svg +1 -1
  141. package/icons/lucide/info.svg +1 -1
  142. package/icons/lucide/keyboard.svg +1 -1
  143. package/icons/lucide/leaf.svg +1 -1
  144. package/icons/lucide/lifebuoy.svg +1 -1
  145. package/icons/lucide/lightbulb.svg +1 -1
  146. package/icons/lucide/link-break.svg +1 -1
  147. package/icons/lucide/link-simple.svg +1 -1
  148. package/icons/lucide/list-bullets.svg +1 -1
  149. package/icons/lucide/list-checks.svg +1 -1
  150. package/icons/lucide/list-numbers.svg +1 -1
  151. package/icons/lucide/list.svg +1 -1
  152. package/icons/lucide/lock-simple-open.svg +1 -1
  153. package/icons/lucide/lock-simple.svg +1 -1
  154. package/icons/lucide/magnifying-glass.svg +1 -1
  155. package/icons/lucide/map-pin.svg +1 -1
  156. package/icons/lucide/map-trifold.svg +1 -1
  157. package/icons/lucide/medal.svg +1 -1
  158. package/icons/lucide/megaphone.svg +1 -1
  159. package/icons/lucide/minus.svg +1 -1
  160. package/icons/lucide/moon.svg +1 -1
  161. package/icons/lucide/newspaper.svg +1 -1
  162. package/icons/lucide/pack.json +23 -23
  163. package/icons/lucide/paint-brush-broad.svg +1 -1
  164. package/icons/lucide/palette.svg +1 -1
  165. package/icons/lucide/paper-plane-tilt.svg +1 -1
  166. package/icons/lucide/pencil-simple.svg +1 -1
  167. package/icons/lucide/phone.svg +1 -1
  168. package/icons/lucide/play-fill.svg +1 -1
  169. package/icons/lucide/plus.svg +1 -1
  170. package/icons/lucide/prohibit.svg +1 -1
  171. package/icons/lucide/puzzle-piece.svg +1 -1
  172. package/icons/lucide/question.svg +1 -1
  173. package/icons/lucide/quotes.svg +1 -1
  174. package/icons/lucide/rows.svg +1 -1
  175. package/icons/lucide/ruler.svg +1 -1
  176. package/icons/lucide/scan.svg +1 -1
  177. package/icons/lucide/shapes.svg +1 -1
  178. package/icons/lucide/share-network.svg +1 -1
  179. package/icons/lucide/shield-check.svg +1 -1
  180. package/icons/lucide/sidebar-simple.svg +1 -1
  181. package/icons/lucide/sign-out.svg +1 -1
  182. package/icons/lucide/signpost.svg +1 -1
  183. package/icons/lucide/sliders-horizontal.svg +1 -1
  184. package/icons/lucide/smiley.svg +1 -1
  185. package/icons/lucide/sparkle.svg +1 -1
  186. package/icons/lucide/square-half-bottom.svg +1 -1
  187. package/icons/lucide/square.svg +1 -1
  188. package/icons/lucide/squares-four.svg +1 -1
  189. package/icons/lucide/sticker.svg +1 -1
  190. package/icons/lucide/storefront.svg +1 -1
  191. package/icons/lucide/sun.svg +1 -1
  192. package/icons/lucide/swatches.svg +1 -1
  193. package/icons/lucide/tag.svg +1 -1
  194. package/icons/lucide/text-aa.svg +1 -1
  195. package/icons/lucide/text-align-left.svg +1 -1
  196. package/icons/lucide/text-b.svg +1 -1
  197. package/icons/lucide/text-h-one.svg +1 -1
  198. package/icons/lucide/text-h-three.svg +1 -1
  199. package/icons/lucide/text-h-two.svg +1 -1
  200. package/icons/lucide/text-italic.svg +1 -1
  201. package/icons/lucide/text-t.svg +1 -1
  202. package/icons/lucide/ticket.svg +1 -1
  203. package/icons/lucide/toggle-right.svg +1 -1
  204. package/icons/lucide/train.svg +1 -1
  205. package/icons/lucide/trash.svg +1 -1
  206. package/icons/lucide/upload-simple.svg +1 -1
  207. package/icons/lucide/user-circle.svg +1 -1
  208. package/icons/lucide/user.svg +1 -1
  209. package/icons/lucide/users-three.svg +1 -1
  210. package/icons/lucide/users.svg +1 -1
  211. package/icons/lucide/video-camera.svg +1 -1
  212. package/icons/lucide/warning-circle.svg +1 -1
  213. package/icons/lucide/warning.svg +1 -1
  214. package/icons/lucide/wave-sine.svg +1 -1
  215. package/icons/lucide/wheelchair.svg +1 -1
  216. package/icons/lucide/wrench.svg +1 -1
  217. package/icons/lucide/x.svg +1 -1
  218. package/icons/material-symbols/LICENSE +201 -201
  219. package/icons/material-symbols/address-book.svg +1 -1
  220. package/icons/material-symbols/app-window.svg +1 -1
  221. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
  222. package/icons/material-symbols/arrow-down.svg +1 -1
  223. package/icons/material-symbols/arrow-left.svg +1 -1
  224. package/icons/material-symbols/arrow-right.svg +1 -1
  225. package/icons/material-symbols/arrow-square-out.svg +1 -1
  226. package/icons/material-symbols/arrow-u-up-left.svg +1 -1
  227. package/icons/material-symbols/arrow-u-up-right.svg +1 -1
  228. package/icons/material-symbols/arrow-up-right.svg +1 -1
  229. package/icons/material-symbols/arrow-up.svg +1 -1
  230. package/icons/material-symbols/arrows-down-up.svg +1 -1
  231. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
  232. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
  233. package/icons/material-symbols/article.svg +1 -1
  234. package/icons/material-symbols/bank.svg +1 -1
  235. package/icons/material-symbols/bell.svg +1 -1
  236. package/icons/material-symbols/bicycle.svg +1 -1
  237. package/icons/material-symbols/binoculars.svg +1 -1
  238. package/icons/material-symbols/book-open-text.svg +1 -1
  239. package/icons/material-symbols/book-open.svg +1 -1
  240. package/icons/material-symbols/browser.svg +1 -1
  241. package/icons/material-symbols/buildings.svg +1 -1
  242. package/icons/material-symbols/bus.svg +1 -1
  243. package/icons/material-symbols/calendar-blank.svg +1 -1
  244. package/icons/material-symbols/calendar-check.svg +1 -1
  245. package/icons/material-symbols/car.svg +1 -1
  246. package/icons/material-symbols/cards.svg +1 -1
  247. package/icons/material-symbols/caret-down.svg +1 -1
  248. package/icons/material-symbols/caret-left.svg +1 -1
  249. package/icons/material-symbols/caret-right.svg +1 -1
  250. package/icons/material-symbols/caret-up.svg +1 -1
  251. package/icons/material-symbols/certificate.svg +1 -1
  252. package/icons/material-symbols/chart-line-up.svg +1 -1
  253. package/icons/material-symbols/chats.svg +1 -1
  254. package/icons/material-symbols/check-circle.svg +1 -1
  255. package/icons/material-symbols/check.svg +1 -1
  256. package/icons/material-symbols/circle-half.svg +1 -1
  257. package/icons/material-symbols/circle.svg +1 -1
  258. package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
  259. package/icons/material-symbols/clock.svg +1 -1
  260. package/icons/material-symbols/code.svg +1 -1
  261. package/icons/material-symbols/coffee.svg +1 -1
  262. package/icons/material-symbols/columns.svg +1 -1
  263. package/icons/material-symbols/cookie.svg +1 -1
  264. package/icons/material-symbols/copy.svg +1 -1
  265. package/icons/material-symbols/credit-card.svg +1 -1
  266. package/icons/material-symbols/currency-circle-dollar.svg +1 -1
  267. package/icons/material-symbols/cursor-click.svg +1 -1
  268. package/icons/material-symbols/database.svg +1 -1
  269. package/icons/material-symbols/desktop.svg +1 -1
  270. package/icons/material-symbols/device-mobile.svg +1 -1
  271. package/icons/material-symbols/door-open.svg +1 -1
  272. package/icons/material-symbols/download-simple.svg +1 -1
  273. package/icons/material-symbols/envelope-simple.svg +1 -1
  274. package/icons/material-symbols/eye-slash.svg +1 -1
  275. package/icons/material-symbols/eye.svg +1 -1
  276. package/icons/material-symbols/file-pdf.svg +1 -1
  277. package/icons/material-symbols/file-text.svg +1 -1
  278. package/icons/material-symbols/file-video.svg +1 -1
  279. package/icons/material-symbols/file-zip.svg +1 -1
  280. package/icons/material-symbols/flower-lotus.svg +1 -1
  281. package/icons/material-symbols/folder-simple.svg +1 -1
  282. package/icons/material-symbols/funnel.svg +1 -1
  283. package/icons/material-symbols/gauge.svg +1 -1
  284. package/icons/material-symbols/gear.svg +1 -1
  285. package/icons/material-symbols/github-logo.svg +1 -1
  286. package/icons/material-symbols/globe.svg +1 -1
  287. package/icons/material-symbols/google-logo.svg +1 -1
  288. package/icons/material-symbols/graduation-cap.svg +1 -1
  289. package/icons/material-symbols/hammer.svg +1 -1
  290. package/icons/material-symbols/hand-heart.svg +1 -1
  291. package/icons/material-symbols/handshake.svg +1 -1
  292. package/icons/material-symbols/hard-drives.svg +1 -1
  293. package/icons/material-symbols/heart.svg +1 -1
  294. package/icons/material-symbols/house.svg +1 -1
  295. package/icons/material-symbols/image-square.svg +1 -1
  296. package/icons/material-symbols/image.svg +1 -1
  297. package/icons/material-symbols/info.svg +1 -1
  298. package/icons/material-symbols/keyboard.svg +1 -1
  299. package/icons/material-symbols/leaf.svg +1 -1
  300. package/icons/material-symbols/lifebuoy.svg +1 -1
  301. package/icons/material-symbols/lightbulb.svg +1 -1
  302. package/icons/material-symbols/link-break.svg +1 -1
  303. package/icons/material-symbols/link-simple.svg +1 -1
  304. package/icons/material-symbols/list-bullets.svg +1 -1
  305. package/icons/material-symbols/list-checks.svg +1 -1
  306. package/icons/material-symbols/list-numbers.svg +1 -1
  307. package/icons/material-symbols/list.svg +1 -1
  308. package/icons/material-symbols/lock-simple-open.svg +1 -1
  309. package/icons/material-symbols/lock-simple.svg +1 -1
  310. package/icons/material-symbols/magnifying-glass.svg +1 -1
  311. package/icons/material-symbols/map-pin.svg +1 -1
  312. package/icons/material-symbols/map-trifold.svg +1 -1
  313. package/icons/material-symbols/medal.svg +1 -1
  314. package/icons/material-symbols/megaphone.svg +1 -1
  315. package/icons/material-symbols/minus.svg +1 -1
  316. package/icons/material-symbols/moon.svg +1 -1
  317. package/icons/material-symbols/newspaper.svg +1 -1
  318. package/icons/material-symbols/pack.json +29 -29
  319. package/icons/material-symbols/paint-brush-broad.svg +1 -1
  320. package/icons/material-symbols/palette.svg +1 -1
  321. package/icons/material-symbols/paper-plane-tilt.svg +1 -1
  322. package/icons/material-symbols/pencil-simple.svg +1 -1
  323. package/icons/material-symbols/phone.svg +1 -1
  324. package/icons/material-symbols/play-fill.svg +1 -1
  325. package/icons/material-symbols/plus.svg +1 -1
  326. package/icons/material-symbols/prohibit.svg +1 -1
  327. package/icons/material-symbols/puzzle-piece.svg +1 -1
  328. package/icons/material-symbols/question.svg +1 -1
  329. package/icons/material-symbols/quotes.svg +1 -1
  330. package/icons/material-symbols/rows.svg +1 -1
  331. package/icons/material-symbols/ruler.svg +1 -1
  332. package/icons/material-symbols/scan.svg +1 -1
  333. package/icons/material-symbols/shapes.svg +1 -1
  334. package/icons/material-symbols/share-network.svg +1 -1
  335. package/icons/material-symbols/shield-check.svg +1 -1
  336. package/icons/material-symbols/sidebar-simple.svg +1 -1
  337. package/icons/material-symbols/sign-out.svg +1 -1
  338. package/icons/material-symbols/signpost.svg +1 -1
  339. package/icons/material-symbols/sliders-horizontal.svg +1 -1
  340. package/icons/material-symbols/smiley.svg +1 -1
  341. package/icons/material-symbols/sparkle.svg +1 -1
  342. package/icons/material-symbols/square-half-bottom.svg +1 -1
  343. package/icons/material-symbols/square.svg +1 -1
  344. package/icons/material-symbols/squares-four.svg +1 -1
  345. package/icons/material-symbols/sticker.svg +1 -1
  346. package/icons/material-symbols/storefront.svg +1 -1
  347. package/icons/material-symbols/sun.svg +1 -1
  348. package/icons/material-symbols/swatches.svg +1 -1
  349. package/icons/material-symbols/tag.svg +1 -1
  350. package/icons/material-symbols/text-aa.svg +1 -1
  351. package/icons/material-symbols/text-align-left.svg +1 -1
  352. package/icons/material-symbols/text-b.svg +1 -1
  353. package/icons/material-symbols/text-h-one.svg +1 -1
  354. package/icons/material-symbols/text-h-three.svg +1 -1
  355. package/icons/material-symbols/text-h-two.svg +1 -1
  356. package/icons/material-symbols/text-italic.svg +1 -1
  357. package/icons/material-symbols/text-t.svg +1 -1
  358. package/icons/material-symbols/ticket.svg +1 -1
  359. package/icons/material-symbols/toggle-right.svg +1 -1
  360. package/icons/material-symbols/train.svg +1 -1
  361. package/icons/material-symbols/trash.svg +1 -1
  362. package/icons/material-symbols/upload-simple.svg +1 -1
  363. package/icons/material-symbols/user-circle.svg +1 -1
  364. package/icons/material-symbols/user.svg +1 -1
  365. package/icons/material-symbols/users-three.svg +1 -1
  366. package/icons/material-symbols/users.svg +1 -1
  367. package/icons/material-symbols/video-camera.svg +1 -1
  368. package/icons/material-symbols/warning-circle.svg +1 -1
  369. package/icons/material-symbols/warning.svg +1 -1
  370. package/icons/material-symbols/wave-sine.svg +1 -1
  371. package/icons/material-symbols/wheelchair.svg +1 -1
  372. package/icons/material-symbols/wrench.svg +1 -1
  373. package/icons/material-symbols/x.svg +1 -1
  374. package/icons/phosphor/pack.json +8 -8
  375. package/lib/apps.mjs +142 -139
  376. package/lib/apps.test.d.mts +1 -0
  377. package/lib/apps.test.mjs +50 -0
  378. package/lib/blocks.mjs +90 -90
  379. package/lib/caseCollisions.d.mts +4 -0
  380. package/lib/caseCollisions.mjs +62 -0
  381. package/lib/caseCollisions.test.d.mts +1 -0
  382. package/lib/caseCollisions.test.mjs +74 -0
  383. package/lib/colors.ts +86 -86
  384. package/lib/contactBar.ts +18 -18
  385. package/lib/contracts.d.mts +15 -0
  386. package/lib/contracts.mjs +17 -0
  387. package/lib/cookieCategories.mjs +28 -28
  388. package/lib/crop.mjs +83 -83
  389. package/lib/csp.mjs +130 -130
  390. package/lib/cspSources.mjs +60 -60
  391. package/lib/dynamic-colors.ts +426 -426
  392. package/lib/git.mjs +64 -64
  393. package/lib/hooks.ts +51 -51
  394. package/lib/inline.ts +20 -20
  395. package/lib/license-texts/Apache-2.0.txt +190 -190
  396. package/lib/license-texts/BSD-2-Clause.txt +24 -24
  397. package/lib/license-texts/BSD-3-Clause.txt +28 -28
  398. package/lib/license-texts/ISC.txt +15 -15
  399. package/lib/license-texts/MIT.txt +21 -21
  400. package/lib/licenses.d.mts +1 -1
  401. package/lib/licenses.mjs +683 -683
  402. package/lib/markdown.ts +42 -42
  403. package/lib/motion-schemes.mjs +5 -5
  404. package/lib/motion.ts +131 -131
  405. package/lib/notice.mjs +34 -34
  406. package/lib/scheduled.mjs +37 -37
  407. package/lib/snackbar.ts +149 -149
  408. package/lib/status.mjs +18 -18
  409. package/lib/storeEntries.d.mts +18 -0
  410. package/lib/storeEntries.mjs +64 -0
  411. package/lib/storeEntries.test.d.mts +1 -0
  412. package/lib/storeEntries.test.mjs +71 -0
  413. package/lib/svg.mjs +231 -231
  414. package/lib/text-colors.mjs +43 -43
  415. package/lib/video-id.mjs +72 -72
  416. package/lib/video.mjs +91 -91
  417. package/package.json +136 -131
  418. package/pages/404.astro +10 -10
  419. package/pages/[...slug].astro +51 -51
  420. package/pages/_headers.txt +31 -31
  421. package/pages/blog/index.astro +10 -10
  422. package/pages/blog/kategori/[category].astro +18 -18
  423. package/pages/editor/code-images.json.js +25 -25
  424. package/pages/editor/index.astro +75 -75
  425. package/pages/editor/manifest.webmanifest.js +23 -23
  426. package/pages/editor/sw.js +33 -33
  427. package/pages/editor/sw.js.js +4 -4
  428. package/pages/editor/thumbnails.json.js +21 -21
  429. package/pages/editor/version.json.js +22 -22
  430. package/pages/headers.js +4 -4
  431. package/pages/index.astro +10 -10
  432. package/pages/ny.astro +6 -6
  433. package/pages/robots.txt.js +3 -3
  434. package/pages/rss.xml.js +20 -20
  435. package/scripts/app-screenshots.mjs +164 -164
  436. package/scripts/app-workers.mjs +6 -6
  437. package/scripts/bin.mjs +12 -12
  438. package/scripts/built-site.mjs +134 -99
  439. package/scripts/check-consent.mjs +113 -113
  440. package/scripts/check-content.mjs +151 -149
  441. package/scripts/check-demo.mjs +24 -24
  442. package/scripts/check-store.mjs +160 -0
  443. package/scripts/check-themes.mjs +45 -45
  444. package/scripts/check-worker.mjs +569 -569
  445. package/scripts/compare-html.mjs +81 -81
  446. package/scripts/declarations.mjs +10 -0
  447. package/scripts/declarations.test.mjs +32 -0
  448. package/scripts/demo-site.mjs +42 -42
  449. package/scripts/editor-icons.mjs +56 -56
  450. package/scripts/editor-roundtrip.mjs +134 -134
  451. package/scripts/editor-screens.mjs +101 -101
  452. package/scripts/git-env.mjs +9 -9
  453. package/scripts/icon-packs.mjs +151 -151
  454. package/scripts/link-counts.mjs +22 -22
  455. package/scripts/page-colors.mjs +42 -42
  456. package/scripts/releases.mjs +80 -80
  457. package/scripts/responsive-images.mjs +94 -94
  458. package/scripts/scheduled-list.mjs +15 -15
  459. package/scripts/theme-screenshots.mjs +88 -88
  460. package/scripts/types.mjs +17 -21
  461. package/scripts/video-thumbnails.mjs +89 -89
  462. package/scripts/worker-modules.mjs +34 -34
  463. package/site/appCookies.mjs +74 -74
  464. package/site/apps.ts +21 -21
  465. package/site/autolinks.mjs +200 -200
  466. package/site/blockScripts.ts +31 -31
  467. package/site/categories.ts +16 -16
  468. package/site/consent.ts +26 -26
  469. package/site/content.config.ts +84 -84
  470. package/site/content.ts +104 -104
  471. package/site/cookieList.ts +17 -17
  472. package/site/defineSite.d.mts +2 -14
  473. package/site/defineSite.mjs +131 -136
  474. package/site/directives/interact.mjs +31 -31
  475. package/site/editor-version.mjs +26 -26
  476. package/site/fill.ts +13 -13
  477. package/site/format.ts +11 -11
  478. package/site/home.ts +66 -66
  479. package/site/iconPacks.mjs +43 -43
  480. package/site/icons.mjs +605 -605
  481. package/site/images.mjs +157 -157
  482. package/site/motion.ts +13 -13
  483. package/site/noindex.mjs +21 -21
  484. package/site/pageColors.ts +110 -110
  485. package/site/popups.ts +98 -98
  486. package/site/schema.ts +189 -189
  487. package/site/sections.ts +3 -3
  488. package/site/seo.ts +28 -28
  489. package/site/site.ts +76 -76
  490. package/site/siteConfig.mjs +8 -8
  491. package/site/siteCsp.mjs +75 -75
  492. package/site/siteModules.mjs +27 -27
  493. package/site/standardPages.ts +59 -59
  494. package/site/storeTheme.d.mts +6 -0
  495. package/site/storeTheme.mjs +29 -0
  496. package/site/storeTheme.test.d.mts +1 -0
  497. package/site/storeTheme.test.mjs +53 -0
  498. package/site/testimonials.ts +48 -48
  499. package/site/text.ts +43 -43
  500. package/site/theme.mjs +483 -472
  501. package/site/tracking.client.ts +225 -225
  502. package/site/tracking.ts +9 -9
  503. package/src/ActionParts.tsx +205 -205
  504. package/src/Advanced.tsx +152 -152
  505. package/src/App.tsx +901 -904
  506. package/src/AppList.tsx +1362 -1345
  507. package/src/BlockDialog.tsx +360 -360
  508. package/src/BlockParts.tsx +652 -652
  509. package/src/BlockSettings.tsx +533 -533
  510. package/src/BrandForm.tsx +164 -164
  511. package/src/Colors.tsx +476 -476
  512. package/src/CommunicationParts.tsx +199 -199
  513. package/src/CompanyForm.tsx +209 -209
  514. package/src/Consents.tsx +175 -175
  515. package/src/ContainmentParts.tsx +185 -185
  516. package/src/Crop.tsx +152 -152
  517. package/src/DataForm.tsx +434 -434
  518. package/src/Deleted.tsx +177 -177
  519. package/src/EntryForm.tsx +740 -740
  520. package/src/FileView.tsx +30 -30
  521. package/src/FormFrame.tsx +237 -237
  522. package/src/Keywords.tsx +225 -225
  523. package/src/Library.tsx +1563 -1563
  524. package/src/Licenses.tsx +336 -336
  525. package/src/LinkSheet.tsx +138 -138
  526. package/src/MarkdownEditor.tsx +1075 -1075
  527. package/src/Media.tsx +677 -677
  528. package/src/NavigationParts.tsx +350 -350
  529. package/src/NavigationRestParts.tsx +247 -247
  530. package/src/Notifications.tsx +59 -59
  531. package/src/PagePicker.tsx +136 -136
  532. package/src/Parts.tsx +991 -991
  533. package/src/PartsShown.tsx +168 -168
  534. package/src/PickerParts.tsx +183 -183
  535. package/src/Redirects.tsx +450 -450
  536. package/src/Screen.tsx +184 -184
  537. package/src/SectionsField.tsx +65 -65
  538. package/src/SelectionParts.tsx +134 -134
  539. package/src/SeoScores.tsx +184 -184
  540. package/src/Sheet.tsx +35 -35
  541. package/src/ThemeStore.tsx +376 -376
  542. package/src/Tokens.tsx +346 -346
  543. package/src/Versions.tsx +214 -214
  544. package/src/addresses.ts +75 -75
  545. package/src/allIcons.ts +7 -7
  546. package/src/appFiles.ts +29 -29
  547. package/src/apps.ts +281 -281
  548. package/src/ask.tsx +123 -123
  549. package/src/blocks.ts +251 -251
  550. package/src/brand.ts +153 -153
  551. package/src/cache.ts +42 -42
  552. package/src/check.ts +285 -285
  553. package/src/checkSite.ts +256 -256
  554. package/src/{consents.ts → consentData.ts} +26 -26
  555. package/src/cookieList.ts +70 -70
  556. package/src/countries.ts +59 -59
  557. package/src/deploy.ts +155 -155
  558. package/src/editor.css +602 -602
  559. package/src/examples.ts +85 -85
  560. package/src/fields.tsx +942 -942
  561. package/src/frontmatter.mjs +42 -42
  562. package/src/github.ts +376 -376
  563. package/src/i18n/da-DK/advanced.ts +28 -28
  564. package/src/i18n/da-DK/app.ts +71 -71
  565. package/src/i18n/da-DK/apps.ts +90 -90
  566. package/src/i18n/da-DK/blockSettings.ts +38 -38
  567. package/src/i18n/da-DK/blocks.ts +408 -408
  568. package/src/i18n/da-DK/check.ts +25 -25
  569. package/src/i18n/da-DK/colors.ts +64 -64
  570. package/src/i18n/da-DK/common.ts +118 -118
  571. package/src/i18n/da-DK/company.ts +24 -24
  572. package/src/i18n/da-DK/consents.ts +23 -23
  573. package/src/i18n/da-DK/forms.ts +116 -116
  574. package/src/i18n/da-DK/history.ts +35 -35
  575. package/src/i18n/da-DK/index.ts +49 -49
  576. package/src/i18n/da-DK/keywords.ts +19 -19
  577. package/src/i18n/da-DK/library.ts +135 -135
  578. package/src/i18n/da-DK/licenses.ts +25 -25
  579. package/src/i18n/da-DK/media.ts +79 -79
  580. package/src/i18n/da-DK/parts.ts +696 -696
  581. package/src/i18n/da-DK/preview.ts +13 -13
  582. package/src/i18n/da-DK/redirects.ts +51 -51
  583. package/src/i18n/da-DK/seo.ts +20 -20
  584. package/src/i18n/da-DK/text.ts +35 -35
  585. package/src/i18n/da-DK/theme.ts +41 -41
  586. package/src/i18n/en-US/advanced.ts +32 -32
  587. package/src/i18n/en-US/app.ts +84 -84
  588. package/src/i18n/en-US/apps.ts +134 -134
  589. package/src/i18n/en-US/blockSettings.ts +48 -48
  590. package/src/i18n/en-US/blocks.ts +441 -441
  591. package/src/i18n/en-US/check.ts +31 -31
  592. package/src/i18n/en-US/colors.ts +82 -82
  593. package/src/i18n/en-US/common.ts +144 -144
  594. package/src/i18n/en-US/company.ts +25 -25
  595. package/src/i18n/en-US/consents.ts +25 -25
  596. package/src/i18n/en-US/forms.ts +145 -145
  597. package/src/i18n/en-US/history.ts +43 -43
  598. package/src/i18n/en-US/index.ts +51 -51
  599. package/src/i18n/en-US/keywords.ts +24 -24
  600. package/src/i18n/en-US/library.ts +176 -176
  601. package/src/i18n/en-US/licenses.ts +36 -36
  602. package/src/i18n/en-US/media.ts +80 -80
  603. package/src/i18n/en-US/parts.ts +718 -718
  604. package/src/i18n/en-US/preview.ts +15 -15
  605. package/src/i18n/en-US/redirects.ts +66 -66
  606. package/src/i18n/en-US/seo.ts +26 -26
  607. package/src/i18n/en-US/text.ts +41 -41
  608. package/src/i18n/en-US/theme.ts +41 -41
  609. package/src/i18n/index.ts +124 -124
  610. package/src/icon.tsx +160 -160
  611. package/src/iconPacks.ts +89 -89
  612. package/src/images.ts +163 -163
  613. package/src/{licenses.ts → licenseData.ts} +79 -79
  614. package/src/markdown.mjs +204 -204
  615. package/src/nav.ts +68 -68
  616. package/src/notFound.ts +28 -28
  617. package/src/{notifications.ts → notificationData.ts} +223 -223
  618. package/src/pagePreview.tsx +204 -204
  619. package/src/r2Caching.ts +55 -55
  620. package/src/{redirects.ts → redirectData.ts} +149 -149
  621. package/src/registry.tsx +188 -188
  622. package/src/schema.ts +417 -417
  623. package/src/sections.mjs +303 -303
  624. package/src/seo.ts +333 -333
  625. package/src/shared.ts +44 -44
  626. package/src/sidebar.ts +31 -31
  627. package/src/site.ts +317 -317
  628. package/src/standard/Block.astro +19 -19
  629. package/src/standard/BlockSite.astro +18 -18
  630. package/src/standard/accordion/index.tsx +47 -47
  631. package/src/standard/accordion/load.ts +23 -23
  632. package/src/standard/accordion/shared.ts +15 -15
  633. package/src/standard/accordion/view.ts +32 -32
  634. package/src/standard/buttons/fields.ts +98 -98
  635. package/src/standard/buttons/index.tsx +23 -23
  636. package/src/standard/buttons/load.ts +34 -34
  637. package/src/standard/buttons/shared.ts +12 -12
  638. package/src/standard/buttons/view.ts +30 -30
  639. package/src/standard/cards/index.tsx +71 -71
  640. package/src/standard/cards/load.ts +17 -17
  641. package/src/standard/cards/shared.ts +27 -27
  642. package/src/standard/cards/view.ts +29 -29
  643. package/src/standard/checklist/index.tsx +27 -27
  644. package/src/standard/checklist/load.ts +5 -5
  645. package/src/standard/checklist/view.ts +13 -13
  646. package/src/standard/cookies/index.tsx +17 -17
  647. package/src/standard/cookies/list.ts +56 -56
  648. package/src/standard/cookies/load.ts +11 -11
  649. package/src/standard/cookies/shared.ts +20 -20
  650. package/src/standard/cookies/view.ts +28 -28
  651. package/src/standard/faq/index.tsx +29 -29
  652. package/src/standard/faq/load.ts +14 -14
  653. package/src/standard/faq/view.ts +21 -21
  654. package/src/standard/head.ts +23 -23
  655. package/src/standard/hero/fields.ts +48 -48
  656. package/src/standard/hero/index.tsx +57 -57
  657. package/src/standard/hero/load.ts +23 -23
  658. package/src/standard/hero/view.ts +38 -38
  659. package/src/standard/links/index.tsx +41 -41
  660. package/src/standard/links/load.ts +17 -17
  661. package/src/standard/links/view.ts +25 -25
  662. package/src/standard/list/index.tsx +50 -50
  663. package/src/standard/list/load.ts +5 -5
  664. package/src/standard/list/view.ts +18 -18
  665. package/src/standard/load.ts +7 -7
  666. package/src/standard/notice/index.tsx +30 -30
  667. package/src/standard/notice/load.ts +16 -16
  668. package/src/standard/notice/view.ts +10 -10
  669. package/src/standard/points/index.tsx +42 -42
  670. package/src/standard/points/load.ts +5 -5
  671. package/src/standard/points/view.ts +12 -12
  672. package/src/standard/posts/index.tsx +132 -132
  673. package/src/standard/posts/load.ts +36 -36
  674. package/src/standard/posts/shared.ts +21 -21
  675. package/src/standard/posts/view.ts +63 -63
  676. package/src/standard/pricecards/index.tsx +57 -57
  677. package/src/standard/pricecards/load.ts +11 -11
  678. package/src/standard/pricecards/view.ts +50 -50
  679. package/src/standard/sharedTexts.ts +6 -6
  680. package/src/standard/site.ts +124 -124
  681. package/src/standard/siteLooks.ts +19 -19
  682. package/src/standard/sites.ts +13 -13
  683. package/src/standard/steps/index.tsx +28 -28
  684. package/src/standard/steps/load.ts +5 -5
  685. package/src/standard/steps/view.ts +11 -11
  686. package/src/standard/testimonials/index.tsx +87 -87
  687. package/src/standard/testimonials/list.ts +43 -43
  688. package/src/standard/testimonials/load.ts +58 -58
  689. package/src/standard/testimonials/shared.ts +13 -13
  690. package/src/standard/testimonials/view.ts +76 -76
  691. package/src/standard/themeBlocks.ts +13 -13
  692. package/src/standard/video/index.tsx +23 -23
  693. package/src/standard/video/load.ts +10 -10
  694. package/src/standard/video/view.ts +42 -42
  695. package/src/standard/view.tsx +105 -105
  696. package/src/standard/views.ts +37 -37
  697. package/src/store.ts +479 -499
  698. package/src/theme.ts +90 -90
  699. package/src/themeUi.ts +97 -97
  700. package/src/themes.ts +91 -91
  701. package/src/ui.tsx +3402 -3402
  702. package/src/virtual.d.ts +5 -5
  703. package/src/when.ts +22 -22
  704. package/starter/.githooks/pre-push +4 -4
  705. package/starter/.nvmrc +1 -1
  706. package/starter/.prettierignore +11 -11
  707. package/starter/.prettierrc.json +7 -7
  708. package/starter/README.md +27 -26
  709. package/starter/astro.config.mjs +5 -5
  710. package/starter/package.json +67 -67
  711. package/starter/public/_redirects +1 -1
  712. package/starter/public/favicon.svg +1 -1
  713. package/starter/public/logo.svg +1 -1
  714. package/starter/src/content/pages/blog.md +8 -8
  715. package/starter/src/content/pages/forside.md +9 -9
  716. package/starter/src/content/pages/kontakt.md +7 -7
  717. package/starter/src/content/pages/privatlivspolitik.md +9 -9
  718. package/starter/src/content/pages/siden-findes-ikke.md +7 -7
  719. package/starter/src/content.config.ts +2 -2
  720. package/starter/src/data/apps.json +1 -1
  721. package/starter/src/data/block-settings.json +3 -3
  722. package/starter/src/data/blocks.json +1 -1
  723. package/starter/src/data/booking.json +6 -6
  724. package/starter/src/data/colors.json +70 -70
  725. package/starter/src/data/contact.json +8 -8
  726. package/starter/src/data/cookies.json +21 -21
  727. package/starter/src/data/csp.json +1 -1
  728. package/starter/src/data/footer.json +15 -15
  729. package/starter/src/data/internal-links.json +4 -4
  730. package/starter/src/data/menu.json +4 -4
  731. package/starter/src/data/mobile-menu.json +4 -4
  732. package/starter/src/data/old-addresses.json +1 -1
  733. package/starter/src/data/redirects.json +1 -1
  734. package/starter/src/data/standard-pages.json +8 -8
  735. package/starter/src/data/theme.json +1 -1
  736. package/starter/src/env.d.ts +2 -2
  737. package/starter/src/site/config.ts +13 -13
  738. package/starter/src/site/worker.js +14 -14
  739. package/starter/tsconfig.json +17 -17
  740. package/starter/worker/apps.js +6 -6
  741. package/starter/wrangler.jsonc +39 -39
  742. package/theme/LICENSE +21 -21
  743. package/theme/blocks/ArrowLink.tsx +14 -14
  744. package/theme/blocks/BlockHead.tsx +26 -26
  745. package/theme/blocks/Panel.tsx +32 -32
  746. package/theme/blocks/Person.tsx +26 -26
  747. package/theme/blocks/accordion/PageRow.tsx +25 -25
  748. package/theme/blocks/accordion/View.tsx +17 -17
  749. package/theme/blocks/buttons/Buttons.tsx +28 -28
  750. package/theme/blocks/buttons/View.tsx +12 -12
  751. package/theme/blocks/cards/LinkCards.tsx +72 -72
  752. package/theme/blocks/cards/View.tsx +20 -20
  753. package/theme/blocks/checklist/Checks.tsx +15 -15
  754. package/theme/blocks/checklist/View.tsx +14 -14
  755. package/theme/blocks/cookies/View.tsx +36 -36
  756. package/theme/blocks/faq/Accordion.tsx +34 -34
  757. package/theme/blocks/faq/View.tsx +16 -16
  758. package/theme/blocks/faq/script.ts +56 -56
  759. package/theme/blocks/hero/View.tsx +44 -44
  760. package/theme/blocks/links/LinkGroups.tsx +24 -24
  761. package/theme/blocks/links/View.tsx +17 -17
  762. package/theme/blocks/list/Rows.tsx +24 -24
  763. package/theme/blocks/list/View.tsx +14 -14
  764. package/theme/blocks/notice/View.tsx +13 -13
  765. package/theme/blocks/points/PointList.tsx +34 -34
  766. package/theme/blocks/points/View.tsx +14 -14
  767. package/theme/blocks/posts/View.tsx +26 -26
  768. package/theme/blocks/pricecards/View.tsx +40 -40
  769. package/theme/blocks/steps/StepList.tsx +23 -23
  770. package/theme/blocks/steps/View.tsx +14 -14
  771. package/theme/blocks/testimonials/List.tsx +46 -46
  772. package/theme/blocks/testimonials/Slider.tsx +44 -44
  773. package/theme/blocks/testimonials/Testimonial.tsx +124 -124
  774. package/theme/blocks/testimonials/View.tsx +21 -21
  775. package/theme/blocks/testimonials/script.ts +52 -52
  776. package/theme/blocks/video/View.tsx +41 -41
  777. package/theme/components/ActionBar.astro +63 -63
  778. package/theme/components/AppSlot.astro +21 -21
  779. package/theme/components/ArrowLink.tsx +21 -21
  780. package/theme/components/BlogIndex.astro +48 -48
  781. package/theme/components/BlogPosts.astro +18 -18
  782. package/theme/components/BlogPosts.tsx +45 -45
  783. package/theme/components/BookButton.tsx +22 -22
  784. package/theme/components/Container.astro +11 -11
  785. package/theme/components/CookieConsent.astro +121 -121
  786. package/theme/components/EndBlock.astro +21 -21
  787. package/theme/components/FontPreload.astro +6 -6
  788. package/theme/components/Footer.astro +85 -85
  789. package/theme/components/Header.astro +186 -186
  790. package/theme/components/Icon.astro +16 -16
  791. package/theme/components/Img.astro +29 -29
  792. package/theme/components/Img.tsx +33 -33
  793. package/theme/components/Inline.astro +11 -11
  794. package/theme/components/Logo.astro +23 -23
  795. package/theme/components/LogoMark.astro +10 -10
  796. package/theme/components/Markdown.astro +17 -17
  797. package/theme/components/MobileMenu.tsx +224 -224
  798. package/theme/components/MobileNav.astro +33 -33
  799. package/theme/components/PageHeader.astro +22 -22
  800. package/theme/components/PageSection.astro +24 -24
  801. package/theme/components/PhoneButton.tsx +11 -11
  802. package/theme/components/Popup.astro +145 -145
  803. package/theme/components/PopupButton.tsx +24 -24
  804. package/theme/components/PopupView.tsx +48 -48
  805. package/theme/components/PostList.tsx +58 -58
  806. package/theme/components/PostMeta.tsx +17 -17
  807. package/theme/components/RichText.astro +28 -28
  808. package/theme/components/Section.astro +29 -29
  809. package/theme/components/SectionHeading.tsx +8 -8
  810. package/theme/components/TextSection.astro +16 -16
  811. package/theme/components/home/PageBlock.astro +19 -19
  812. package/theme/components/home/Text.astro +13 -13
  813. package/theme/components/loadPopup.ts +22 -22
  814. package/theme/components/loadPosts.ts +16 -16
  815. package/theme/components/posts.ts +15 -15
  816. package/theme/editor/looks.mjs +7 -7
  817. package/theme/editor/previews.tsx +56 -56
  818. package/theme/layouts/Base.astro +273 -273
  819. package/theme/layouts/PageLayout.astro +126 -126
  820. package/theme/layouts/PostLayout.astro +137 -137
  821. package/theme/scripts/motion.ts +29 -29
  822. package/theme/styles/base.css +320 -320
  823. package/theme/styles/blocks.css +1808 -1808
  824. package/theme/styles/components.css +206 -206
  825. package/theme/styles/global.css +1302 -1302
  826. package/theme/styles/ui.css +6544 -6544
  827. package/theme/templates/Blog.astro +38 -38
  828. package/theme/templates/Category.astro +19 -19
  829. package/theme/templates/Home.astro +36 -36
  830. package/theme/templates/New.astro +68 -68
  831. package/theme/templates/NotFound.astro +117 -117
  832. package/theme/theme.json +28 -28
  833. package/theme/ui/Badge.tsx +45 -45
  834. package/theme/ui/BottomSheet.tsx +148 -148
  835. package/theme/ui/Button.tsx +79 -79
  836. package/theme/ui/ButtonGroup.tsx +31 -31
  837. package/theme/ui/Card.tsx +45 -45
  838. package/theme/ui/Carousel.tsx +124 -124
  839. package/theme/ui/Checkbox.tsx +59 -59
  840. package/theme/ui/Chip.tsx +101 -101
  841. package/theme/ui/ChipSet.tsx +27 -27
  842. package/theme/ui/DatePicker.tsx +149 -149
  843. package/theme/ui/DatePickerContainer.tsx +316 -316
  844. package/theme/ui/Dialog.tsx +100 -100
  845. package/theme/ui/Divider.tsx +27 -27
  846. package/theme/ui/ExposedDropdownMenu.tsx +77 -77
  847. package/theme/ui/Fab.tsx +88 -88
  848. package/theme/ui/FabMenu.tsx +67 -67
  849. package/theme/ui/IconButton.tsx +78 -78
  850. package/theme/ui/List.tsx +287 -287
  851. package/theme/ui/LoadingIndicator.tsx +29 -29
  852. package/theme/ui/Menu.tsx +278 -278
  853. package/theme/ui/NavigationBar.tsx +62 -62
  854. package/theme/ui/NavigationDrawer.tsx +80 -80
  855. package/theme/ui/NavigationRail.tsx +154 -154
  856. package/theme/ui/ProgressIndicator.tsx +162 -162
  857. package/theme/ui/RadioButton.tsx +50 -50
  858. package/theme/ui/Search.tsx +129 -129
  859. package/theme/ui/SegmentedButton.tsx +49 -49
  860. package/theme/ui/SideSheet.tsx +54 -54
  861. package/theme/ui/Slider.tsx +155 -155
  862. package/theme/ui/Snackbar.tsx +67 -67
  863. package/theme/ui/SplitButton.tsx +67 -67
  864. package/theme/ui/Switch.tsx +60 -60
  865. package/theme/ui/Tabs.tsx +114 -114
  866. package/theme/ui/TextField.tsx +278 -278
  867. package/theme/ui/TimePicker.tsx +128 -128
  868. package/theme/ui/TimePickerContainer.tsx +234 -234
  869. package/theme/ui/Toolbar.tsx +67 -67
  870. package/theme/ui/Tooltip.tsx +58 -58
  871. package/theme/ui/TopAppBar.tsx +77 -77
  872. package/theme/ui/bottomSheet.script.ts +8 -8
  873. package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
  874. package/theme/ui/carousel.script.ts +28 -28
  875. package/theme/ui/carouselLayout.ts +229 -229
  876. package/theme/ui/checkbox.script.ts +8 -8
  877. package/theme/ui/datePicker.script.ts +302 -302
  878. package/theme/ui/dates.ts +185 -185
  879. package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
  880. package/theme/ui/dropdownMenu.script.ts +166 -166
  881. package/theme/ui/fabMenu.script.ts +57 -57
  882. package/theme/ui/navigationRail.script.ts +47 -47
  883. package/theme/ui/ripple.script.ts +159 -159
  884. package/theme/ui/search.script.ts +92 -92
  885. package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
  886. package/theme/ui/slider.script.ts +12 -12
  887. package/theme/ui/sliderUpdate.ts +52 -52
  888. package/theme/ui/snackbar.script.ts +16 -16
  889. package/theme/ui/splitButton.script.ts +63 -63
  890. package/theme/ui/tabs.script.ts +48 -48
  891. package/theme/ui/textField.script.ts +17 -17
  892. package/theme/ui/textFieldUpdate.ts +44 -44
  893. package/theme/ui/timePicker.script.ts +248 -248
  894. package/theme/ui/toolbar.script.ts +43 -43
  895. package/theme/ui/tooltip.script.ts +68 -68
  896. package/worker/consent.js +141 -141
  897. package/worker/editor-github.js +167 -167
  898. package/worker/editor-host.js +74 -74
  899. package/worker/files.js +401 -401
  900. package/worker/github.js +36 -36
  901. package/worker/index.js +188 -184
  902. package/worker/not-found.js +97 -97
  903. package/worker/oauth.js +95 -95
  904. package/worker/publish.js +52 -52
  905. package/worker/push.js +270 -270
  906. package/worker/r2-status.js +50 -50
  907. package/worker/session.js +58 -58
  908. package/worker/store.js +46 -0
package/worker/push.js CHANGED
@@ -1,270 +1,270 @@
1
- // The editor's notifications on the phone or computer also when the editor is closed: Web Push from the Worker
2
- // (packages/editor/src/notifications.ts, packages/editor/pages/editor/sw.js). A device where "På telefonen" is on under "Konto" ›
3
- // "Notifikationer" signs up here with the browser's push subscription, the types it has turned off and the editor's
4
- // language; the Worker sends to the browser's push service (Google's, Mozilla's, Apple's), which wakes the editor's
5
- // service worker, which shows it. Kept in Workers KV (the binding PUSH in wrangler.jsonc):
6
- // vapid the Worker's own key pair for the push services (VAPID, RFC 8292), made the first time it is needed;
7
- // a device signed up with another key signs up again when the editor opens
8
- // device:<id> a device: its subscription, the types turned off and the language
9
- // wait:<id> the device's last save, until the site is built with it: the commit and the two texts to send
10
- // waiting until when there may be a wait (so the cron each minute asks KV once, and lists only then)
11
- // What is sent:
12
- // - "Hjemmesiden er opdateret med dine ændringer" (type `site`) to the device that saved, when the site has been built
13
- // with the save (each minute, waitForBuilds: the cron in wrangler.jsonc), or that it was not, after ten minutes;
14
- // the texts are the editor's, in its language. An editor on screen says it itself and takes the wait back.
15
- // - an app's notification from its part of the Worker (`notify` in the tools it is given, packages/editor/worker/index.js) to each
16
- // device with the type on, in the device's language (the app's texts.ts).
17
- // The payload is encrypted for the device (RFC 8291, aes128gcm); a device the push service no longer knows is removed.
18
- // GET /api/push { key }: the Worker's public key, for the browser's subscription
19
- // PUT /api/push { subscription, off, locale }: signs a device up, or updates its choices
20
- // DELETE /api/push { endpoint }: signs it off ("På telefonen" off, "Log ud")
21
- // PUT /api/push/wait { endpoint, commit, text, failText, to? }: sends `text` when the site is built with `commit`
22
- // DELETE /api/push/wait { endpoint }: takes it back (the editor said it itself)
23
- // Only someone who can save the site (write access to the repository on GitHub) can use them. Without the binding (a
24
- // preview of a branch) nothing is kept or sent, and the editor's notifications come only while it is open.
25
- import { config } from '@site/config';
26
- import { contact } from '@site/worker';
27
- import { json } from './not-found.js';
28
- import { mayEdit } from './session.js';
29
-
30
- const VAPID = 'vapid';
31
- const DEVICE = 'device:';
32
- const WAIT = 'wait:';
33
- const WAITING = 'waiting';
34
- // How long the editor waits for a save to be on the site (packages/editor/src/deploy.ts GIVE_UP).
35
- const GIVE_UP = 10 * 60_000;
36
- // A device not seen (the editor not opened on it) for half a year drops out by itself.
37
- const KEEP = 180 * 24 * 60 * 60;
38
- // How long a push service keeps a notification for a device that is off.
39
- const TTL = 24 * 60 * 60;
40
-
41
- const encoder = new TextEncoder();
42
-
43
- function b64url(bytes) {
44
- let binary = '';
45
- for (const byte of bytes) binary += String.fromCharCode(byte);
46
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
47
- }
48
-
49
- function fromB64url(text) {
50
- const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
51
- return Uint8Array.from(binary, (c) => c.charCodeAt(0));
52
- }
53
-
54
- const concat = (...parts) => {
55
- const all = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
56
- let at = 0;
57
- for (const p of parts) all.set(p, (at += p.length) - p.length);
58
- return all;
59
- };
60
-
61
- /** A device's id: a hash of its endpoint, so the endpoint is not a key. */
62
- async function idOf(endpoint) {
63
- const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(endpoint)));
64
- return b64url(hash.slice(0, 16));
65
- }
66
-
67
- /** The Worker's key pair as a JWK with the private part, made and kept the first time. */
68
- async function vapidOf(kv) {
69
- const kept = await kv.get(VAPID, 'json');
70
- if (kept) return kept;
71
- const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
72
- const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
73
- await kv.put(VAPID, JSON.stringify(jwk));
74
- return jwk;
75
- }
76
-
77
- /** The public key as the browser takes it (an uncompressed point, base64url). */
78
- const publicKeyOf = (jwk) => b64url(concat(new Uint8Array([4]), fromB64url(jwk.x), fromB64url(jwk.y)));
79
-
80
- /** The header that says the push comes from this site (VAPID, RFC 8292). */
81
- async function vapidHeader(jwk, endpoint) {
82
- const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
83
- const part = (o) => b64url(encoder.encode(JSON.stringify(o)));
84
- const claims = { aud: new URL(endpoint).origin, exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, sub: `https://${config.domain}` };
85
- const unsigned = `${part({ typ: 'JWT', alg: 'ES256' })}.${part(claims)}`;
86
- const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, encoder.encode(unsigned));
87
- return `vapid t=${unsigned}.${b64url(new Uint8Array(signature))}, k=${publicKeyOf(jwk)}`;
88
- }
89
-
90
- async function hkdf(salt, ikm, info, bytes) {
91
- const key = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
92
- return new Uint8Array(await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, key, bytes * 8));
93
- }
94
-
95
- /** The payload encrypted for the device (RFC 8291, the content coding aes128gcm of RFC 8188, one record).
96
- * `salt` and `pair` are for the tests only. */
97
- export async function encrypt(payload, { p256dh, auth }, { salt = crypto.getRandomValues(new Uint8Array(16)), pair } = {}) {
98
- const device = fromB64url(p256dh);
99
- const own = pair ?? (await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']));
100
- const ownPublic = new Uint8Array(await crypto.subtle.exportKey('raw', own.publicKey));
101
- const deviceKey = await crypto.subtle.importKey('raw', device, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
102
- const shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: deviceKey }, own.privateKey, 256));
103
- const ikm = await hkdf(fromB64url(auth), shared, concat(encoder.encode('WebPush: info\0'), device, ownPublic), 32);
104
- const cek = await hkdf(salt, ikm, encoder.encode('Content-Encoding: aes128gcm\0'), 16);
105
- const nonce = await hkdf(salt, ikm, encoder.encode('Content-Encoding: nonce\0'), 12);
106
- const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
107
- // The last (and only) record ends with 2.
108
- const plain = concat(encoder.encode(payload), new Uint8Array([2]));
109
- const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, plain));
110
- const size = new Uint8Array(4);
111
- new DataView(size.buffer).setUint32(0, 4096);
112
- return concat(salt, size, new Uint8Array([ownPublic.length]), ownPublic, sealed);
113
- }
114
-
115
- /** Sends a notification ({ title, body, to, tag, error }) to a device; removes the device when its push service no
116
- * longer knows it. */
117
- async function send(env, id, device, message) {
118
- const { endpoint, keys } = device.subscription;
119
- try {
120
- const jwk = await vapidOf(env.PUSH);
121
- const res = await fetch(endpoint, {
122
- method: 'POST',
123
- headers: {
124
- authorization: await vapidHeader(jwk, endpoint),
125
- 'content-encoding': 'aes128gcm',
126
- 'content-type': 'application/octet-stream',
127
- ttl: String(TTL),
128
- urgency: 'normal',
129
- },
130
- body: await encrypt(JSON.stringify({ title: contact.name, ...message }), keys),
131
- });
132
- if (res.status === 404 || res.status === 410) await env.PUSH.delete(DEVICE + id);
133
- else if (!res.ok) console.error(`Notifikationen kunne ikke sendes: ${res.status} ${(await res.text()).slice(0, 200)}`);
134
- } catch (e) {
135
- console.error(`Notifikationen kunne ikke sendes: ${e?.message ?? e}`);
136
- }
137
- }
138
-
139
- /** The devices signed up, by their ids. */
140
- async function devices(kv) {
141
- const all = [];
142
- let cursor;
143
- for (let i = 0; i < 5; i++) {
144
- const page = await kv.list({ prefix: DEVICE, cursor });
145
- all.push(...page.keys.map((k) => k.name.slice(DEVICE.length)));
146
- if (page.list_complete) break;
147
- cursor = page.cursor;
148
- }
149
- const kept = await Promise.all(all.map((id) => kv.get(DEVICE + id, 'json')));
150
- return all.map((id, i) => ({ id, device: kept[i] })).filter((d) => d.device?.subscription);
151
- }
152
-
153
- /** Sends an app's notification ({ type, id, text, error, to }, its type as `<app>/<type>`) to each device with the type
154
- * on, its text in the device's language (`texts`: the app's texts.ts). */
155
- export async function notifyAll(env, { type, id, text, error, to }, texts = {}) {
156
- if (!env.PUSH) return;
157
- for (const { id: device, device: d } of await devices(env.PUSH)) {
158
- if (d.off?.includes(type)) continue;
159
- await send(env, device, d, { body: texts[d.locale]?.[text] ?? text, error: !!error, to, tag: `${type}:${id}` });
160
- }
161
- }
162
-
163
- /** What the site was last built from (/editor/version.json, packages/editor/pages/editor/version.json.js). */
164
- async function builtOf(env) {
165
- try {
166
- const res = await env.ASSETS.fetch(new Request('https://assets.local/editor/version.json'));
167
- return res.ok ? await res.json() : {};
168
- } catch {
169
- return {};
170
- }
171
- }
172
-
173
- /** Whether the build's commit has `commit` in it (it may be built from a later one): asked of GitHub with the Worker's
174
- * own token (GITHUB_PUBLISH_TOKEN); without it, only the commit itself counts. */
175
- async function includes(env, build, commit) {
176
- if (build === commit) return true;
177
- if (!env.GITHUB_PUBLISH_TOKEN) return false;
178
- const { owner, name } = config.repo;
179
- const res = await fetch(`https://api.github.com/repos/${owner}/${name}/compare/${commit}...${build}`, {
180
- headers: { authorization: `Bearer ${env.GITHUB_PUBLISH_TOKEN}`, accept: 'application/vnd.github+json', 'user-agent': `${name}-worker` },
181
- });
182
- if (!res.ok) return false;
183
- const { status } = await res.json().catch(() => ({}));
184
- return status === 'ahead' || status === 'identical';
185
- }
186
-
187
- /** Each minute: tells each device waiting for its save whether the site has been built with it (see above). */
188
- export async function waitForBuilds(env, now = Date.now()) {
189
- if (!env.PUSH) return;
190
- const until = Number(await env.PUSH.get(WAITING));
191
- if (!until || now > until) return;
192
- const { keys } = await env.PUSH.list({ prefix: WAIT });
193
- if (!keys.length) return;
194
- const built = await builtOf(env);
195
- const builtAt = Date.parse(built.builtAt ?? '');
196
- for (const { name, metadata: wait } of keys) {
197
- if (!wait) continue;
198
- const at = Date.parse(wait.at);
199
- // A build made before the save cannot have it.
200
- const online = !!built.commit && builtAt >= at && (await includes(env, built.commit, wait.commit).catch(() => false));
201
- const late = now - at > GIVE_UP;
202
- if (!online && !late) continue;
203
- await env.PUSH.delete(name);
204
- const device = await env.PUSH.get(DEVICE + wait.device, 'json');
205
- // Without any version file (a build from before it existed) there is nothing to tell, as in the editor.
206
- if (!device?.subscription || device.off?.includes('site') || (!online && !built.commit)) continue;
207
- await send(env, wait.device, device, { body: online ? wait.text : wait.failText, error: !online, to: wait.to, tag: 'site' });
208
- }
209
- }
210
-
211
- const text = (value, max = 300) => (typeof value === 'string' ? value.slice(0, max) : '');
212
- const isRoute = (to) => typeof to === 'string' && /^[\w\-/æøåÆØÅ]{1,200}$/.test(to);
213
-
214
- // The browsers' push services (Chrome's, Firefox's, Safari's, Edge's): the Worker sends to no other address.
215
- const PUSH_SERVICE =
216
- /^https:\/\/(fcm\.googleapis\.com|android\.googleapis\.com|updates\.push\.services\.mozilla\.com|[\w-]+(\.[\w-]+)*\.push\.apple\.com|[\w-]+(\.[\w-]+)*\.notify\.windows\.com)\//;
217
-
218
- function subscriptionOf(value) {
219
- const { endpoint, keys } = value ?? {};
220
- if (typeof endpoint !== 'string' || !PUSH_SERVICE.test(endpoint) || endpoint.length > 1000) return null;
221
- if (typeof keys?.p256dh !== 'string' || typeof keys?.auth !== 'string') return null;
222
- return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
223
- }
224
-
225
- /** The addresses /api/push and /api/push/wait (see above). */
226
- export async function handlePush(request, env) {
227
- if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
228
- if (!env.PUSH) return json({ error: 'unavailable' }, 503);
229
- const wait = new URL(request.url).pathname.replace(/\/$/, '') === '/api/push/wait';
230
- if (request.method === 'GET' && !wait) return json({ key: publicKeyOf(await vapidOf(env.PUSH)) });
231
- const body = await request.json().catch(() => ({}));
232
- const endpoint = body.endpoint ?? body.subscription?.endpoint;
233
- if (typeof endpoint !== 'string' || !endpoint) return json({ error: 'endpoint' }, 400);
234
- const id = await idOf(endpoint);
235
-
236
- if (wait && request.method === 'PUT') {
237
- if (typeof body.commit !== 'string' || !/^[0-9a-f]{40}$/.test(body.commit)) return json({ error: 'commit' }, 400);
238
- const at = new Date().toISOString();
239
- const metadata = { commit: body.commit, device: id, text: text(body.text), failText: text(body.failText), at };
240
- if (isRoute(body.to)) metadata.to = body.to;
241
- // The value is empty: all of it is in the metadata, which the list each minute gives with the key.
242
- await env.PUSH.put(WAIT + id, '', { metadata, expirationTtl: (2 * GIVE_UP) / 1000 });
243
- await env.PUSH.put(WAITING, String(Date.now() + GIVE_UP + 2 * 60_000));
244
- return json({ ok: true });
245
- }
246
- if (wait && request.method === 'DELETE') {
247
- await env.PUSH.delete(WAIT + id);
248
- return json({ ok: true });
249
- }
250
- if (wait) return json({ error: 'method' }, 405);
251
-
252
- if (request.method === 'PUT') {
253
- const subscription = subscriptionOf(body.subscription);
254
- if (!subscription) return json({ error: 'subscription' }, 400);
255
- const off = Array.isArray(body.off)
256
- ? body.off
257
- .filter((t) => typeof t === 'string')
258
- .slice(0, 100)
259
- .map((t) => text(t, 100))
260
- : [];
261
- const device = { subscription, off, locale: text(body.locale, 20), at: new Date().toISOString() };
262
- await env.PUSH.put(DEVICE + id, JSON.stringify(device), { expirationTtl: KEEP });
263
- return json({ ok: true });
264
- }
265
- if (request.method === 'DELETE') {
266
- await Promise.all([env.PUSH.delete(DEVICE + id), env.PUSH.delete(WAIT + id)]);
267
- return json({ ok: true });
268
- }
269
- return json({ error: 'method' }, 405);
270
- }
1
+ // The editor's notifications on the phone or computer also when the editor is closed: Web Push from the Worker
2
+ // (packages/editor/src/notificationData.ts, packages/editor/pages/editor/sw.js). A device where "På telefonen" is on under "Konto" ›
3
+ // "Notifikationer" signs up here with the browser's push subscription, the types it has turned off and the editor's
4
+ // language; the Worker sends to the browser's push service (Google's, Mozilla's, Apple's), which wakes the editor's
5
+ // service worker, which shows it. Kept in Workers KV (the binding PUSH in wrangler.jsonc):
6
+ // vapid the Worker's own key pair for the push services (VAPID, RFC 8292), made the first time it is needed;
7
+ // a device signed up with another key signs up again when the editor opens
8
+ // device:<id> a device: its subscription, the types turned off and the language
9
+ // wait:<id> the device's last save, until the site is built with it: the commit and the two texts to send
10
+ // waiting until when there may be a wait (so the cron each minute asks KV once, and lists only then)
11
+ // What is sent:
12
+ // - "Hjemmesiden er opdateret med dine ændringer" (type `site`) to the device that saved, when the site has been built
13
+ // with the save (each minute, waitForBuilds: the cron in wrangler.jsonc), or that it was not, after ten minutes;
14
+ // the texts are the editor's, in its language. An editor on screen says it itself and takes the wait back.
15
+ // - an app's notification from its part of the Worker (`notify` in the tools it is given, packages/editor/worker/index.js) to each
16
+ // device with the type on, in the device's language (the app's texts.ts).
17
+ // The payload is encrypted for the device (RFC 8291, aes128gcm); a device the push service no longer knows is removed.
18
+ // GET /api/push { key }: the Worker's public key, for the browser's subscription
19
+ // PUT /api/push { subscription, off, locale }: signs a device up, or updates its choices
20
+ // DELETE /api/push { endpoint }: signs it off ("På telefonen" off, "Log ud")
21
+ // PUT /api/push/wait { endpoint, commit, text, failText, to? }: sends `text` when the site is built with `commit`
22
+ // DELETE /api/push/wait { endpoint }: takes it back (the editor said it itself)
23
+ // Only someone who can save the site (write access to the repository on GitHub) can use them. Without the binding (a
24
+ // preview of a branch) nothing is kept or sent, and the editor's notifications come only while it is open.
25
+ import { config } from '@site/config';
26
+ import { contact } from '@site/worker';
27
+ import { json } from './not-found.js';
28
+ import { mayEdit } from './session.js';
29
+
30
+ const VAPID = 'vapid';
31
+ const DEVICE = 'device:';
32
+ const WAIT = 'wait:';
33
+ const WAITING = 'waiting';
34
+ // How long the editor waits for a save to be on the site (packages/editor/src/deploy.ts GIVE_UP).
35
+ const GIVE_UP = 10 * 60_000;
36
+ // A device not seen (the editor not opened on it) for half a year drops out by itself.
37
+ const KEEP = 180 * 24 * 60 * 60;
38
+ // How long a push service keeps a notification for a device that is off.
39
+ const TTL = 24 * 60 * 60;
40
+
41
+ const encoder = new TextEncoder();
42
+
43
+ function b64url(bytes) {
44
+ let binary = '';
45
+ for (const byte of bytes) binary += String.fromCharCode(byte);
46
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
47
+ }
48
+
49
+ function fromB64url(text) {
50
+ const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
51
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
52
+ }
53
+
54
+ const concat = (...parts) => {
55
+ const all = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
56
+ let at = 0;
57
+ for (const p of parts) all.set(p, (at += p.length) - p.length);
58
+ return all;
59
+ };
60
+
61
+ /** A device's id: a hash of its endpoint, so the endpoint is not a key. */
62
+ async function idOf(endpoint) {
63
+ const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(endpoint)));
64
+ return b64url(hash.slice(0, 16));
65
+ }
66
+
67
+ /** The Worker's key pair as a JWK with the private part, made and kept the first time. */
68
+ async function vapidOf(kv) {
69
+ const kept = await kv.get(VAPID, 'json');
70
+ if (kept) return kept;
71
+ const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
72
+ const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
73
+ await kv.put(VAPID, JSON.stringify(jwk));
74
+ return jwk;
75
+ }
76
+
77
+ /** The public key as the browser takes it (an uncompressed point, base64url). */
78
+ const publicKeyOf = (jwk) => b64url(concat(new Uint8Array([4]), fromB64url(jwk.x), fromB64url(jwk.y)));
79
+
80
+ /** The header that says the push comes from this site (VAPID, RFC 8292). */
81
+ async function vapidHeader(jwk, endpoint) {
82
+ const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
83
+ const part = (o) => b64url(encoder.encode(JSON.stringify(o)));
84
+ const claims = { aud: new URL(endpoint).origin, exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, sub: `https://${config.domain}` };
85
+ const unsigned = `${part({ typ: 'JWT', alg: 'ES256' })}.${part(claims)}`;
86
+ const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, encoder.encode(unsigned));
87
+ return `vapid t=${unsigned}.${b64url(new Uint8Array(signature))}, k=${publicKeyOf(jwk)}`;
88
+ }
89
+
90
+ async function hkdf(salt, ikm, info, bytes) {
91
+ const key = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
92
+ return new Uint8Array(await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, key, bytes * 8));
93
+ }
94
+
95
+ /** The payload encrypted for the device (RFC 8291, the content coding aes128gcm of RFC 8188, one record).
96
+ * `salt` and `pair` are for the tests only. */
97
+ export async function encrypt(payload, { p256dh, auth }, { salt = crypto.getRandomValues(new Uint8Array(16)), pair } = {}) {
98
+ const device = fromB64url(p256dh);
99
+ const own = pair ?? (await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']));
100
+ const ownPublic = new Uint8Array(await crypto.subtle.exportKey('raw', own.publicKey));
101
+ const deviceKey = await crypto.subtle.importKey('raw', device, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
102
+ const shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: deviceKey }, own.privateKey, 256));
103
+ const ikm = await hkdf(fromB64url(auth), shared, concat(encoder.encode('WebPush: info\0'), device, ownPublic), 32);
104
+ const cek = await hkdf(salt, ikm, encoder.encode('Content-Encoding: aes128gcm\0'), 16);
105
+ const nonce = await hkdf(salt, ikm, encoder.encode('Content-Encoding: nonce\0'), 12);
106
+ const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
107
+ // The last (and only) record ends with 2.
108
+ const plain = concat(encoder.encode(payload), new Uint8Array([2]));
109
+ const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, plain));
110
+ const size = new Uint8Array(4);
111
+ new DataView(size.buffer).setUint32(0, 4096);
112
+ return concat(salt, size, new Uint8Array([ownPublic.length]), ownPublic, sealed);
113
+ }
114
+
115
+ /** Sends a notification ({ title, body, to, tag, error }) to a device; removes the device when its push service no
116
+ * longer knows it. */
117
+ async function send(env, id, device, message) {
118
+ const { endpoint, keys } = device.subscription;
119
+ try {
120
+ const jwk = await vapidOf(env.PUSH);
121
+ const res = await fetch(endpoint, {
122
+ method: 'POST',
123
+ headers: {
124
+ authorization: await vapidHeader(jwk, endpoint),
125
+ 'content-encoding': 'aes128gcm',
126
+ 'content-type': 'application/octet-stream',
127
+ ttl: String(TTL),
128
+ urgency: 'normal',
129
+ },
130
+ body: await encrypt(JSON.stringify({ title: contact.name, ...message }), keys),
131
+ });
132
+ if (res.status === 404 || res.status === 410) await env.PUSH.delete(DEVICE + id);
133
+ else if (!res.ok) console.error(`Notifikationen kunne ikke sendes: ${res.status} ${(await res.text()).slice(0, 200)}`);
134
+ } catch (e) {
135
+ console.error(`Notifikationen kunne ikke sendes: ${e?.message ?? e}`);
136
+ }
137
+ }
138
+
139
+ /** The devices signed up, by their ids. */
140
+ async function devices(kv) {
141
+ const all = [];
142
+ let cursor;
143
+ for (let i = 0; i < 5; i++) {
144
+ const page = await kv.list({ prefix: DEVICE, cursor });
145
+ all.push(...page.keys.map((k) => k.name.slice(DEVICE.length)));
146
+ if (page.list_complete) break;
147
+ cursor = page.cursor;
148
+ }
149
+ const kept = await Promise.all(all.map((id) => kv.get(DEVICE + id, 'json')));
150
+ return all.map((id, i) => ({ id, device: kept[i] })).filter((d) => d.device?.subscription);
151
+ }
152
+
153
+ /** Sends an app's notification ({ type, id, text, error, to }, its type as `<app>/<type>`) to each device with the type
154
+ * on, its text in the device's language (`texts`: the app's texts.ts). */
155
+ export async function notifyAll(env, { type, id, text, error, to }, texts = {}) {
156
+ if (!env.PUSH) return;
157
+ for (const { id: device, device: d } of await devices(env.PUSH)) {
158
+ if (d.off?.includes(type)) continue;
159
+ await send(env, device, d, { body: texts[d.locale]?.[text] ?? text, error: !!error, to, tag: `${type}:${id}` });
160
+ }
161
+ }
162
+
163
+ /** What the site was last built from (/editor/version.json, packages/editor/pages/editor/version.json.js). */
164
+ async function builtOf(env) {
165
+ try {
166
+ const res = await env.ASSETS.fetch(new Request('https://assets.local/editor/version.json'));
167
+ return res.ok ? await res.json() : {};
168
+ } catch {
169
+ return {};
170
+ }
171
+ }
172
+
173
+ /** Whether the build's commit has `commit` in it (it may be built from a later one): asked of GitHub with the Worker's
174
+ * own token (GITHUB_PUBLISH_TOKEN); without it, only the commit itself counts. */
175
+ async function includes(env, build, commit) {
176
+ if (build === commit) return true;
177
+ if (!env.GITHUB_PUBLISH_TOKEN) return false;
178
+ const { owner, name } = config.repo;
179
+ const res = await fetch(`https://api.github.com/repos/${owner}/${name}/compare/${commit}...${build}`, {
180
+ headers: { authorization: `Bearer ${env.GITHUB_PUBLISH_TOKEN}`, accept: 'application/vnd.github+json', 'user-agent': `${name}-worker` },
181
+ });
182
+ if (!res.ok) return false;
183
+ const { status } = await res.json().catch(() => ({}));
184
+ return status === 'ahead' || status === 'identical';
185
+ }
186
+
187
+ /** Each minute: tells each device waiting for its save whether the site has been built with it (see above). */
188
+ export async function waitForBuilds(env, now = Date.now()) {
189
+ if (!env.PUSH) return;
190
+ const until = Number(await env.PUSH.get(WAITING));
191
+ if (!until || now > until) return;
192
+ const { keys } = await env.PUSH.list({ prefix: WAIT });
193
+ if (!keys.length) return;
194
+ const built = await builtOf(env);
195
+ const builtAt = Date.parse(built.builtAt ?? '');
196
+ for (const { name, metadata: wait } of keys) {
197
+ if (!wait) continue;
198
+ const at = Date.parse(wait.at);
199
+ // A build made before the save cannot have it.
200
+ const online = !!built.commit && builtAt >= at && (await includes(env, built.commit, wait.commit).catch(() => false));
201
+ const late = now - at > GIVE_UP;
202
+ if (!online && !late) continue;
203
+ await env.PUSH.delete(name);
204
+ const device = await env.PUSH.get(DEVICE + wait.device, 'json');
205
+ // Without any version file (a build from before it existed) there is nothing to tell, as in the editor.
206
+ if (!device?.subscription || device.off?.includes('site') || (!online && !built.commit)) continue;
207
+ await send(env, wait.device, device, { body: online ? wait.text : wait.failText, error: !online, to: wait.to, tag: 'site' });
208
+ }
209
+ }
210
+
211
+ const text = (value, max = 300) => (typeof value === 'string' ? value.slice(0, max) : '');
212
+ const isRoute = (to) => typeof to === 'string' && /^[\w\-/æøåÆØÅ]{1,200}$/.test(to);
213
+
214
+ // The browsers' push services (Chrome's, Firefox's, Safari's, Edge's): the Worker sends to no other address.
215
+ const PUSH_SERVICE =
216
+ /^https:\/\/(fcm\.googleapis\.com|android\.googleapis\.com|updates\.push\.services\.mozilla\.com|[\w-]+(\.[\w-]+)*\.push\.apple\.com|[\w-]+(\.[\w-]+)*\.notify\.windows\.com)\//;
217
+
218
+ function subscriptionOf(value) {
219
+ const { endpoint, keys } = value ?? {};
220
+ if (typeof endpoint !== 'string' || !PUSH_SERVICE.test(endpoint) || endpoint.length > 1000) return null;
221
+ if (typeof keys?.p256dh !== 'string' || typeof keys?.auth !== 'string') return null;
222
+ return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
223
+ }
224
+
225
+ /** The addresses /api/push and /api/push/wait (see above). */
226
+ export async function handlePush(request, env) {
227
+ if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
228
+ if (!env.PUSH) return json({ error: 'unavailable' }, 503);
229
+ const wait = new URL(request.url).pathname.replace(/\/$/, '') === '/api/push/wait';
230
+ if (request.method === 'GET' && !wait) return json({ key: publicKeyOf(await vapidOf(env.PUSH)) });
231
+ const body = await request.json().catch(() => ({}));
232
+ const endpoint = body.endpoint ?? body.subscription?.endpoint;
233
+ if (typeof endpoint !== 'string' || !endpoint) return json({ error: 'endpoint' }, 400);
234
+ const id = await idOf(endpoint);
235
+
236
+ if (wait && request.method === 'PUT') {
237
+ if (typeof body.commit !== 'string' || !/^[0-9a-f]{40}$/.test(body.commit)) return json({ error: 'commit' }, 400);
238
+ const at = new Date().toISOString();
239
+ const metadata = { commit: body.commit, device: id, text: text(body.text), failText: text(body.failText), at };
240
+ if (isRoute(body.to)) metadata.to = body.to;
241
+ // The value is empty: all of it is in the metadata, which the list each minute gives with the key.
242
+ await env.PUSH.put(WAIT + id, '', { metadata, expirationTtl: (2 * GIVE_UP) / 1000 });
243
+ await env.PUSH.put(WAITING, String(Date.now() + GIVE_UP + 2 * 60_000));
244
+ return json({ ok: true });
245
+ }
246
+ if (wait && request.method === 'DELETE') {
247
+ await env.PUSH.delete(WAIT + id);
248
+ return json({ ok: true });
249
+ }
250
+ if (wait) return json({ error: 'method' }, 405);
251
+
252
+ if (request.method === 'PUT') {
253
+ const subscription = subscriptionOf(body.subscription);
254
+ if (!subscription) return json({ error: 'subscription' }, 400);
255
+ const off = Array.isArray(body.off)
256
+ ? body.off
257
+ .filter((t) => typeof t === 'string')
258
+ .slice(0, 100)
259
+ .map((t) => text(t, 100))
260
+ : [];
261
+ const device = { subscription, off, locale: text(body.locale, 20), at: new Date().toISOString() };
262
+ await env.PUSH.put(DEVICE + id, JSON.stringify(device), { expirationTtl: KEEP });
263
+ return json({ ok: true });
264
+ }
265
+ if (request.method === 'DELETE') {
266
+ await Promise.all([env.PUSH.delete(DEVICE + id), env.PUSH.delete(WAIT + id)]);
267
+ return json({ ok: true });
268
+ }
269
+ return json({ error: 'method' }, 405);
270
+ }