@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/src/github.ts CHANGED
@@ -1,376 +1,376 @@
1
- // GitHub for the editor, through the site's Worker (packages/editor/worker/editor-github.js): the sign-in (packages/editor/worker/oauth.js) is a
2
- // cookie only the Worker can read (packages/editor/worker/session.js), so the browser never has the token, and every question to GitHub
3
- // goes through the Worker with it. The site's files come from the Worker's cache in R2 (packages/editor/worker/files.js) by their git
4
- // ids, and from this browser when it has them: a file's text is asked for once. Only people with write access to the
5
- // repository can save; GitHub checks that on every commit.
6
-
7
- import { config } from '@site/config';
8
- import { blobId } from '../lib/git.mjs';
9
- import { t } from './i18n';
10
-
11
- export const REPO = config.repo;
12
-
13
- // Where the token was kept before the sign-in moved into the Worker's cookie: removed, so it is not left readable.
14
- const OLD_TOKEN_KEY = 'editor-github-token';
15
- try {
16
- localStorage.removeItem(OLD_TOKEN_KEY);
17
- } catch {}
18
-
19
- // "Log ud": the Worker removes the cookie, and GitHub forgets the token.
20
- export async function logout() {
21
- await fetch('/api/github/logout', { method: 'POST' }).catch(() => {});
22
- }
23
-
24
- // Sign in: the Worker sends the browser to GitHub and back, keeps the token in its cookie and opens
25
- // /editor/ again (packages/editor/worker/oauth.js). A redirect, not a popup, so it works in the installed app too.
26
- export function login() {
27
- location.assign('/oauth/auth?provider=github&scope=repo');
28
- }
29
-
30
- export class AuthError extends Error {}
31
-
32
- export class ConflictError extends Error {}
33
-
34
- // A question to the Worker's /api/github/<path>, and its answer in JSON.
35
- async function api<T>(path: string, body?: unknown): Promise<T> {
36
- const res = await fetch(`/api/github/${path}`, {
37
- cache: 'no-store',
38
- ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }),
39
- });
40
- if (res.status === 401) throw new AuthError(t.app.loggedOut);
41
- const json = await res.json().catch(() => ({}));
42
- if (res.status === 409) throw new ConflictError(json.conflict === 'exists' ? t.app.fileExists : t.app.changedElsewhere);
43
- if (!res.ok) throw new Error(json.error ?? t.app.githubAnswered(json.status ?? res.status));
44
- return json;
45
- }
46
-
47
- export async function graphql<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
48
- const res = await fetch('/api/github/graphql', {
49
- method: 'POST',
50
- headers: { 'content-type': 'application/json' },
51
- body: JSON.stringify({ query, variables }),
52
- });
53
- if (res.status === 401) throw new AuthError(t.app.loggedOut);
54
- const json = await res.json().catch(() => ({}));
55
- if (!res.ok || json.errors?.length) throw new Error(json.errors?.[0]?.message ?? t.app.githubAnswered(res.status));
56
- return json.data;
57
- }
58
-
59
- export type Viewer = { login: string; name: string | null; canSave: boolean };
60
-
61
- export async function viewer(): Promise<Viewer> {
62
- const data = await graphql<{ viewer: { login: string; name: string | null }; repository: { viewerPermission: string } | null }>(
63
- `
64
- query ($owner: String!, $name: String!) {
65
- viewer {
66
- login
67
- name
68
- }
69
- repository(owner: $owner, name: $name) {
70
- viewerPermission
71
- }
72
- }
73
- `,
74
- { owner: REPO.owner, name: REPO.name },
75
- );
76
- const permission = data.repository?.viewerPermission ?? '';
77
- return { ...data.viewer, canSave: ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission) };
78
- }
79
-
80
- // One file in the repository as the editor loaded it: its path, text and git blob id. The blob id
81
- // tells whether the file has been changed elsewhere (another browser, a push) since.
82
- export type RepoFile = { path: string; oid: string; text: string };
83
-
84
- /** The commit the branch is at, and its root tree (the files in it). */
85
- export type Head = { commit: string; tree: string };
86
-
87
- // A file as the Worker lists it: its blob id, and whether GitHub calls it binary (a picture: no text).
88
- type Listed = { path: string; oid: string; binary: boolean };
89
-
90
- // The texts of the files this browser has, by their blob ids (a blob id's text never changes): those it loaded and
91
- // saved, and those it kept (cache.ts), so a file's text is asked for once.
92
- const known = new Map<string, string>();
93
-
94
- /** Keeps the files' texts by their blob ids (the files from this browser's copy, cache.ts). */
95
- export function rememberTexts(files: RepoFile[]) {
96
- for (const f of files) known.set(f.oid, f.text);
97
- }
98
-
99
- // The files with their text: '' for a picture, the rest from this browser or the Worker, 100 at a time.
100
- async function withTexts(listed: Listed[]): Promise<RepoFile[]> {
101
- const wanted = [...new Set(listed.filter((f) => !f.binary && !known.has(f.oid)).map((f) => f.oid))];
102
- const parts = [];
103
- for (let i = 0; i < wanted.length; i += 100) parts.push(wanted.slice(i, i + 100));
104
- await Promise.all(
105
- parts.map(async (oids) => {
106
- const { texts } = await api<{ texts: Record<string, string | null> }>('texts', { oids });
107
- for (const [oid, text] of Object.entries(texts)) known.set(oid, text ?? '');
108
- }),
109
- );
110
- return listed.map((f) => ({ path: f.path, oid: f.oid, text: f.binary ? '' : (known.get(f.oid) ?? '') }));
111
- }
112
-
113
- type Files = Head & { folders: Listed[][]; paths: (Listed | null)[] };
114
-
115
- // The files in folders and at paths, at a root tree (`tree`), or at the branch's head.
116
- const listFiles = (ask: { tree?: string; folders?: string[]; depth?: number; paths?: string[] }) => api<Files>('files', ask);
117
-
118
- /** The commit the branch is at: a small request, to see whether the files have changed (cache.ts). */
119
- export const head = () => api<Head>('head');
120
-
121
- // All files in some folders, with their text, and the commit the branch is at. `depth`: how many
122
- // levels of folders (an app's folder, store.ts, has more). `at`: the branch's head, when it was just asked.
123
- export async function loadFolders<K extends string>(
124
- folders: Record<K, string>,
125
- depth = 3,
126
- at?: Head,
127
- ): Promise<{ folders: Record<K, RepoFile[]>; commit: string }> {
128
- const keys = Object.keys(folders) as K[];
129
- const listed = await listFiles({ tree: at?.tree, folders: keys.map((k) => folders[k]), depth });
130
- const files = await withTexts(listed.folders.flat());
131
- const byPath = new Map(files.map((f) => [f.path, f]));
132
- return {
133
- folders: Object.fromEntries(keys.map((k, i) => [k, listed.folders[i].map((f) => byPath.get(f.path)!)])) as Record<K, RepoFile[]>,
134
- commit: at?.commit ?? listed.commit,
135
- };
136
- }
137
-
138
- // Some single files at the branch's head (`tree`: at another root tree), with their text ('' for a picture). A file that
139
- // is not there is left out.
140
- export async function loadFiles(paths: string[], tree?: string): Promise<RepoFile[]> {
141
- const listed = await listFiles({ tree, paths });
142
- return withTexts(listed.paths.filter((f): f is Listed => !!f));
143
- }
144
-
145
- /** A repository on GitHub: the site's (REPO), or the app store's (store.ts). */
146
- export type Repo = { owner: string; name: string };
147
-
148
- // Some files of a repository at a branch or a commit (an app in the store, store.ts), in one request: their blob
149
- // id and their text, or null when they are not text or GitHub gives only part of it (a big file). A file that is not
150
- // there is left out.
151
- export async function loadFrom(repo: Repo, ref: string, paths: string[]): Promise<{ path: string; oid: string; text: string | null }[]> {
152
- if (!paths.length) return [];
153
- const data = await graphql<{
154
- repository: Record<
155
- string,
156
- { oid: string; text?: string | null; isBinary?: boolean | null; isTruncated?: boolean | null } | null
157
- > | null;
158
- }>(
159
- `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) {
160
- ${paths.map((p, i) => `f${i}: object(expression: ${JSON.stringify(`${ref}:${p}`)}) { oid ... on Blob { text isBinary isTruncated } }`).join('\n')}
161
- } }`,
162
- { owner: repo.owner, name: repo.name },
163
- );
164
- return paths.flatMap((path, i) => {
165
- const f = data.repository?.[`f${i}`];
166
- return f ? [{ path, oid: f.oid, text: f.isBinary || f.isTruncated || f.text == null ? null : f.text }] : [];
167
- });
168
- }
169
-
170
- // A file's bytes by its blob id: a picture, which GraphQL gives no content for (renaming one in
171
- // Filer). Checked against the id, so a picture is never saved broken. `repo`: another repository than the site's
172
- // (a picture of an app in the store).
173
- export async function loadBytes(oid: string, address?: string, repo: Repo = REPO): Promise<Uint8Array> {
174
- // First from the site itself (the same file, no request to the Worker), when it has it unchanged.
175
- if (address) {
176
- const res = await fetch(address).catch(() => null);
177
- const bytes = res?.ok ? new Uint8Array(await res.arrayBuffer()) : null;
178
- if (bytes && (await blobId(bytes)) === oid) return bytes;
179
- }
180
- const other = repo.owner !== REPO.owner || repo.name !== REPO.name;
181
- const res = await fetch(`/api/github/blob/${oid}${other ? `?repo=${repo.owner}/${repo.name}` : ''}`).catch(() => {
182
- throw new Error(t.app.pictureNotLoaded);
183
- });
184
- if (res.status === 401) throw new AuthError(t.app.loggedOut);
185
- if (!res.ok) throw new Error(t.app.githubAnswered(res.status));
186
- const bytes = new Uint8Array(await res.arrayBuffer());
187
- if ((await blobId(bytes)) !== oid) throw new Error(t.app.pictureNotWhole);
188
- return bytes;
189
- }
190
-
191
- // When some files were last saved: the time of the last commit that changed each, in one request. A
192
- // new file (no commit yet) is left out.
193
- export async function savedAt(paths: string[]): Promise<Record<string, Date>> {
194
- if (!paths.length) return {};
195
- type History = { nodes: { committedDate: string }[] };
196
- const data = await graphql<{ repository: { ref: { target: Record<string, History> } } }>(
197
- `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) {
198
- ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
199
- ${paths.map((p, i) => `f${i}: history(first: 1, path: ${JSON.stringify(p)}) { nodes { committedDate } }`).join('\n')}
200
- } } }
201
- } }`,
202
- { owner: REPO.owner, name: REPO.name },
203
- );
204
- const target = data.repository.ref.target;
205
- return Object.fromEntries(
206
- paths.flatMap((path, i) => {
207
- const last = target[`f${i}`]?.nodes[0];
208
- return last ? [[path, new Date(last.committedDate)]] : [];
209
- }),
210
- );
211
- }
212
-
213
- // A file to save: text or bytes (null deletes it), and the blob id it had when loaded (null for a
214
- // new file).
215
- export type Change = { path: string; content: string | Uint8Array | null; oid: string | null };
216
-
217
- // Saves files as one commit on the branch, through the Worker. Refuses (ConflictError) when one of them was changed or
218
- // created elsewhere since it was loaded, so nobody's edit is overwritten. Returns the id of the new
219
- // commit and the new blob ids.
220
- export type Saved = { commit: string; blobs: Record<string, string> };
221
-
222
- export async function commit(message: string, changes: Change[]): Promise<Saved> {
223
- const saved = await api<Saved>('commit', {
224
- message,
225
- changes: changes.map((c) => ({
226
- path: c.path,
227
- oid: c.oid,
228
- content: c.content === null ? null : base64(typeof c.content === 'string' ? new TextEncoder().encode(c.content) : c.content),
229
- })),
230
- });
231
- // The texts saved are known by their new blob ids.
232
- for (const c of changes) if (typeof c.content === 'string' && saved.blobs[c.path]) known.set(saved.blobs[c.path], c.content);
233
- return saved;
234
- }
235
-
236
- function base64(bytes: Uint8Array): string {
237
- let binary = '';
238
- for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
239
- return btoa(binary);
240
- }
241
-
242
- // ---------------------------------------------------------------------------------------------------
243
- // The history: earlier versions of a file, and files that were deleted (Versions.tsx, Deleted.tsx). Which commits
244
- // changed a file is asked of GitHub; the folders and files in them come through the Worker's cache, like the files now.
245
-
246
- /** A commit that changed a file: its id, its root tree (the files in it), when, and who saved it (their name on GitHub). */
247
- export type Version = { commit: string; tree: string; at: Date; by: string };
248
-
249
- type HistoryNode = {
250
- oid: string;
251
- tree: { oid: string };
252
- committedDate: string;
253
- author: { name: string | null; user: { name: string | null; login: string } | null };
254
- };
255
-
256
- const NODE = 'oid tree { oid } committedDate author { name user { name login } }';
257
-
258
- const versionOf = (n: HistoryNode): Version => ({
259
- commit: n.oid,
260
- tree: n.tree.oid,
261
- at: new Date(n.committedDate),
262
- by: n.author.user?.name || n.author.name || n.author.user?.login || '',
263
- });
264
-
265
- /** The commits that changed a file, newest first, a page at a time (`after`: where the last ended). */
266
- export async function versionsOf(path: string, after?: string): Promise<{ versions: Version[]; more: string | null }> {
267
- const data = await graphql<{
268
- repository: {
269
- ref: { target: { history: { nodes: HistoryNode[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } };
270
- };
271
- }>(
272
- `query($owner: String!, $name: String!, $path: String!, $after: String) { repository(owner: $owner, name: $name) {
273
- ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
274
- history(first: 20, path: $path, after: $after) {
275
- nodes { ${NODE} }
276
- pageInfo { hasNextPage endCursor }
277
- }
278
- } } }
279
- } }`,
280
- { owner: REPO.owner, name: REPO.name, path, after: after ?? null },
281
- );
282
- const { nodes, pageInfo } = data.repository.ref.target.history;
283
- return { versions: nodes.map(versionOf), more: pageInfo.hasNextPage ? pageInfo.endCursor : null };
284
- }
285
-
286
- /** A file's text as it was in a version; null when it was not there (deleted in that commit). */
287
- export async function versionText(version: Version, path: string): Promise<string | null> {
288
- const [file] = await loadFiles([path], version.tree);
289
- return file ? file.text : null;
290
- }
291
-
292
- /** A file that was deleted: its path, its last content (blob id, and text unless it is a picture),
293
- * and when and by whom it was deleted. */
294
- export type DeletedFile = { path: string; oid: string; text: string; at: Date; by: string };
295
-
296
- type Entry = { name: string; oid: string; type: string; binary: boolean };
297
-
298
- // The folders at paths under root trees (`path` '' the root itself), 200 to a request: their ids and what is in them
299
- // (names, ids and kinds), or null where there is none.
300
- async function foldersAt(at: { root: string; path: string }[]): Promise<({ oid: string; entries: Entry[] } | null)[]> {
301
- const parts = [];
302
- for (let i = 0; i < at.length; i += 200) parts.push(at.slice(i, i + 200));
303
- const answers = await Promise.all(
304
- parts.map((part) => api<{ trees: ({ oid: string; entries: Entry[] } | null)[] }>('trees', { at: part })),
305
- );
306
- return answers.flatMap((a) => a.trees);
307
- }
308
-
309
- /** The files deleted from a folder (and the folders in it) in its last `last` commits, newest first,
310
- * each once, and not one that is there again. A file that went in a commit that also added one to the
311
- * folder moved (a new address, a picture's new name) and is not counted. `text`: whether to get the
312
- * files' text (not for pictures). */
313
- export async function deletedFiles(folder: string, { last = 40, text = true } = {}): Promise<DeletedFile[]> {
314
- const history = await graphql<{
315
- repository: { ref: { target: { history: { nodes: (HistoryNode & { parents: { nodes: { tree: { oid: string } }[] } })[] } } } };
316
- }>(
317
- `query($owner: String!, $name: String!, $path: String!) { repository(owner: $owner, name: $name) {
318
- ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
319
- history(first: ${last}, path: $path) {
320
- nodes { ${NODE} parents(first: 1) { nodes { tree { oid } } } }
321
- }
322
- } } }
323
- } }`,
324
- { owner: REPO.owner, name: REPO.name, path: folder },
325
- );
326
- const commits = history.repository.ref.target.history.nodes;
327
- if (!commits.length) return [];
328
- // The folder in each commit and in the commit before it.
329
- const sides = commits.flatMap((c) => [c.tree.oid, c.parents.nodes[0]?.tree.oid]);
330
- const roots = await foldersAt(sides.flatMap((root) => (root ? [{ root, path: folder }] : [])));
331
- const found = new Map<string, Entry[]>(roots.flatMap((r) => (r ? [[r.oid, r.entries] as const] : [])));
332
- let next = 0;
333
- const rootOid = sides.map((root) => (root ? (roots[next++]?.oid ?? null) : null));
334
-
335
- // What each commit changed in the folder, a level of folders at a time: the trees that differ are
336
- // compared again, the same ones left alone.
337
- type Pair = { commit: number; path: string; now: string | null; was: string | null };
338
- const removed: { commit: number; path: string; oid: string; binary: boolean }[] = [];
339
- const added = new Set<number>();
340
- let pairs: Pair[] = commits.map((_, i) => ({ commit: i, path: folder, now: rootOid[2 * i], was: rootOid[2 * i + 1] }));
341
- pairs = pairs.filter((p) => p.now !== p.was);
342
- while (pairs.length) {
343
- const wanted = [...new Set(pairs.flatMap((p) => [p.now, p.was].filter((o): o is string => !!o && !found.has(o))))];
344
- for (const t of await foldersAt(wanted.map((root) => ({ root, path: '' })))) if (t) found.set(t.oid, t.entries);
345
- const next: Pair[] = [];
346
- for (const p of pairs) {
347
- const now = new Map((p.now ? (found.get(p.now) ?? []) : []).map((e) => [e.name, e]));
348
- const was = new Map((p.was ? (found.get(p.was) ?? []) : []).map((e) => [e.name, e]));
349
- for (const [name, e] of was) {
350
- const there = now.get(name);
351
- if (there?.oid === e.oid) continue;
352
- const path = `${p.path}/${name}`;
353
- if (e.type === 'tree') next.push({ commit: p.commit, path, now: there?.type === 'tree' ? there.oid : null, was: e.oid });
354
- else if (!there) removed.push({ commit: p.commit, path, oid: e.oid, binary: e.binary });
355
- }
356
- for (const [name, e] of now) if (!was.has(name) && (e.type === 'blob' || e.type === 'tree')) added.add(p.commit);
357
- }
358
- pairs = next;
359
- }
360
-
361
- // The newest deletion of each file (the commits are newest first), not one that moved.
362
- const seen = new Set<string>();
363
- const gone = removed.sort((a, b) => a.commit - b.commit).filter((r) => !added.has(r.commit) && !seen.has(r.path) && seen.add(r.path));
364
- if (!gone.length) return [];
365
- // Still there (put back since, or another file under the same name): not deleted.
366
- const now = (await listFiles({ paths: gone.map((r) => r.path) })).paths;
367
- const deleted = gone.filter((_, i) => !now[i]);
368
- if (!deleted.length) return [];
369
- const files = text
370
- ? await withTexts(deleted.map((r) => ({ path: r.path, oid: r.oid, binary: r.binary })))
371
- : deleted.map((r) => ({ text: '' }));
372
- return deleted.map((r, i) => {
373
- const { at, by } = versionOf(commits[r.commit]);
374
- return { path: r.path, oid: r.oid, text: files[i].text, at, by };
375
- });
376
- }
1
+ // GitHub for the editor, through the site's Worker (packages/editor/worker/editor-github.js): the sign-in (packages/editor/worker/oauth.js) is a
2
+ // cookie only the Worker can read (packages/editor/worker/session.js), so the browser never has the token, and every question to GitHub
3
+ // goes through the Worker with it. The site's files come from the Worker's cache in R2 (packages/editor/worker/files.js) by their git
4
+ // ids, and from this browser when it has them: a file's text is asked for once. Only people with write access to the
5
+ // repository can save; GitHub checks that on every commit.
6
+
7
+ import { config } from '@site/config';
8
+ import { blobId } from '../lib/git.mjs';
9
+ import { t } from './i18n';
10
+
11
+ export const REPO = config.repo;
12
+
13
+ // Where the token was kept before the sign-in moved into the Worker's cookie: removed, so it is not left readable.
14
+ const OLD_TOKEN_KEY = 'editor-github-token';
15
+ try {
16
+ localStorage.removeItem(OLD_TOKEN_KEY);
17
+ } catch {}
18
+
19
+ // "Log ud": the Worker removes the cookie, and GitHub forgets the token.
20
+ export async function logout() {
21
+ await fetch('/api/github/logout', { method: 'POST' }).catch(() => {});
22
+ }
23
+
24
+ // Sign in: the Worker sends the browser to GitHub and back, keeps the token in its cookie and opens
25
+ // /editor/ again (packages/editor/worker/oauth.js). A redirect, not a popup, so it works in the installed app too.
26
+ export function login() {
27
+ location.assign('/oauth/auth?provider=github&scope=repo');
28
+ }
29
+
30
+ export class AuthError extends Error {}
31
+
32
+ export class ConflictError extends Error {}
33
+
34
+ // A question to the Worker's /api/github/<path>, and its answer in JSON.
35
+ async function api<T>(path: string, body?: unknown): Promise<T> {
36
+ const res = await fetch(`/api/github/${path}`, {
37
+ cache: 'no-store',
38
+ ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }),
39
+ });
40
+ if (res.status === 401) throw new AuthError(t.app.loggedOut);
41
+ const json = await res.json().catch(() => ({}));
42
+ if (res.status === 409) throw new ConflictError(json.conflict === 'exists' ? t.app.fileExists : t.app.changedElsewhere);
43
+ if (!res.ok) throw new Error(json.error ?? t.app.githubAnswered(json.status ?? res.status));
44
+ return json;
45
+ }
46
+
47
+ export async function graphql<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
48
+ const res = await fetch('/api/github/graphql', {
49
+ method: 'POST',
50
+ headers: { 'content-type': 'application/json' },
51
+ body: JSON.stringify({ query, variables }),
52
+ });
53
+ if (res.status === 401) throw new AuthError(t.app.loggedOut);
54
+ const json = await res.json().catch(() => ({}));
55
+ if (!res.ok || json.errors?.length) throw new Error(json.errors?.[0]?.message ?? t.app.githubAnswered(res.status));
56
+ return json.data;
57
+ }
58
+
59
+ export type Viewer = { login: string; name: string | null; canSave: boolean };
60
+
61
+ export async function viewer(): Promise<Viewer> {
62
+ const data = await graphql<{ viewer: { login: string; name: string | null }; repository: { viewerPermission: string } | null }>(
63
+ `
64
+ query ($owner: String!, $name: String!) {
65
+ viewer {
66
+ login
67
+ name
68
+ }
69
+ repository(owner: $owner, name: $name) {
70
+ viewerPermission
71
+ }
72
+ }
73
+ `,
74
+ { owner: REPO.owner, name: REPO.name },
75
+ );
76
+ const permission = data.repository?.viewerPermission ?? '';
77
+ return { ...data.viewer, canSave: ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission) };
78
+ }
79
+
80
+ // One file in the repository as the editor loaded it: its path, text and git blob id. The blob id
81
+ // tells whether the file has been changed elsewhere (another browser, a push) since.
82
+ export type RepoFile = { path: string; oid: string; text: string };
83
+
84
+ /** The commit the branch is at, and its root tree (the files in it). */
85
+ export type Head = { commit: string; tree: string };
86
+
87
+ // A file as the Worker lists it: its blob id, and whether GitHub calls it binary (a picture: no text).
88
+ type Listed = { path: string; oid: string; binary: boolean };
89
+
90
+ // The texts of the files this browser has, by their blob ids (a blob id's text never changes): those it loaded and
91
+ // saved, and those it kept (cache.ts), so a file's text is asked for once.
92
+ const known = new Map<string, string>();
93
+
94
+ /** Keeps the files' texts by their blob ids (the files from this browser's copy, cache.ts). */
95
+ export function rememberTexts(files: RepoFile[]) {
96
+ for (const f of files) known.set(f.oid, f.text);
97
+ }
98
+
99
+ // The files with their text: '' for a picture, the rest from this browser or the Worker, 100 at a time.
100
+ async function withTexts(listed: Listed[]): Promise<RepoFile[]> {
101
+ const wanted = [...new Set(listed.filter((f) => !f.binary && !known.has(f.oid)).map((f) => f.oid))];
102
+ const parts = [];
103
+ for (let i = 0; i < wanted.length; i += 100) parts.push(wanted.slice(i, i + 100));
104
+ await Promise.all(
105
+ parts.map(async (oids) => {
106
+ const { texts } = await api<{ texts: Record<string, string | null> }>('texts', { oids });
107
+ for (const [oid, text] of Object.entries(texts)) known.set(oid, text ?? '');
108
+ }),
109
+ );
110
+ return listed.map((f) => ({ path: f.path, oid: f.oid, text: f.binary ? '' : (known.get(f.oid) ?? '') }));
111
+ }
112
+
113
+ type Files = Head & { folders: Listed[][]; paths: (Listed | null)[] };
114
+
115
+ // The files in folders and at paths, at a root tree (`tree`), or at the branch's head.
116
+ const listFiles = (ask: { tree?: string; folders?: string[]; depth?: number; paths?: string[] }) => api<Files>('files', ask);
117
+
118
+ /** The commit the branch is at: a small request, to see whether the files have changed (cache.ts). */
119
+ export const head = () => api<Head>('head');
120
+
121
+ // All files in some folders, with their text, and the commit the branch is at. `depth`: how many
122
+ // levels of folders (an app's folder, store.ts, has more). `at`: the branch's head, when it was just asked.
123
+ export async function loadFolders<K extends string>(
124
+ folders: Record<K, string>,
125
+ depth = 3,
126
+ at?: Head,
127
+ ): Promise<{ folders: Record<K, RepoFile[]>; commit: string }> {
128
+ const keys = Object.keys(folders) as K[];
129
+ const listed = await listFiles({ tree: at?.tree, folders: keys.map((k) => folders[k]), depth });
130
+ const files = await withTexts(listed.folders.flat());
131
+ const byPath = new Map(files.map((f) => [f.path, f]));
132
+ return {
133
+ folders: Object.fromEntries(keys.map((k, i) => [k, listed.folders[i].map((f) => byPath.get(f.path)!)])) as Record<K, RepoFile[]>,
134
+ commit: at?.commit ?? listed.commit,
135
+ };
136
+ }
137
+
138
+ // Some single files at the branch's head (`tree`: at another root tree), with their text ('' for a picture). A file that
139
+ // is not there is left out.
140
+ export async function loadFiles(paths: string[], tree?: string): Promise<RepoFile[]> {
141
+ const listed = await listFiles({ tree, paths });
142
+ return withTexts(listed.paths.filter((f): f is Listed => !!f));
143
+ }
144
+
145
+ /** A repository on GitHub: the site's (REPO), or the app store's (store.ts). */
146
+ export type Repo = { owner: string; name: string };
147
+
148
+ // Some files of a repository at a branch or a commit (an app in the store, store.ts), in one request: their blob
149
+ // id and their text, or null when they are not text or GitHub gives only part of it (a big file). A file that is not
150
+ // there is left out.
151
+ export async function loadFrom(repo: Repo, ref: string, paths: string[]): Promise<{ path: string; oid: string; text: string | null }[]> {
152
+ if (!paths.length) return [];
153
+ const data = await graphql<{
154
+ repository: Record<
155
+ string,
156
+ { oid: string; text?: string | null; isBinary?: boolean | null; isTruncated?: boolean | null } | null
157
+ > | null;
158
+ }>(
159
+ `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) {
160
+ ${paths.map((p, i) => `f${i}: object(expression: ${JSON.stringify(`${ref}:${p}`)}) { oid ... on Blob { text isBinary isTruncated } }`).join('\n')}
161
+ } }`,
162
+ { owner: repo.owner, name: repo.name },
163
+ );
164
+ return paths.flatMap((path, i) => {
165
+ const f = data.repository?.[`f${i}`];
166
+ return f ? [{ path, oid: f.oid, text: f.isBinary || f.isTruncated || f.text == null ? null : f.text }] : [];
167
+ });
168
+ }
169
+
170
+ // A file's bytes by its blob id: a picture, which GraphQL gives no content for (renaming one in
171
+ // Filer). Checked against the id, so a picture is never saved broken. `repo`: another repository than the site's
172
+ // (a picture of an app in the store).
173
+ export async function loadBytes(oid: string, address?: string, repo: Repo = REPO): Promise<Uint8Array> {
174
+ // First from the site itself (the same file, no request to the Worker), when it has it unchanged.
175
+ if (address) {
176
+ const res = await fetch(address).catch(() => null);
177
+ const bytes = res?.ok ? new Uint8Array(await res.arrayBuffer()) : null;
178
+ if (bytes && (await blobId(bytes)) === oid) return bytes;
179
+ }
180
+ const other = repo.owner !== REPO.owner || repo.name !== REPO.name;
181
+ const res = await fetch(`/api/github/blob/${oid}${other ? `?repo=${repo.owner}/${repo.name}` : ''}`).catch(() => {
182
+ throw new Error(t.app.pictureNotLoaded);
183
+ });
184
+ if (res.status === 401) throw new AuthError(t.app.loggedOut);
185
+ if (!res.ok) throw new Error(t.app.githubAnswered(res.status));
186
+ const bytes = new Uint8Array(await res.arrayBuffer());
187
+ if ((await blobId(bytes)) !== oid) throw new Error(t.app.pictureNotWhole);
188
+ return bytes;
189
+ }
190
+
191
+ // When some files were last saved: the time of the last commit that changed each, in one request. A
192
+ // new file (no commit yet) is left out.
193
+ export async function savedAt(paths: string[]): Promise<Record<string, Date>> {
194
+ if (!paths.length) return {};
195
+ type History = { nodes: { committedDate: string }[] };
196
+ const data = await graphql<{ repository: { ref: { target: Record<string, History> } } }>(
197
+ `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) {
198
+ ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
199
+ ${paths.map((p, i) => `f${i}: history(first: 1, path: ${JSON.stringify(p)}) { nodes { committedDate } }`).join('\n')}
200
+ } } }
201
+ } }`,
202
+ { owner: REPO.owner, name: REPO.name },
203
+ );
204
+ const target = data.repository.ref.target;
205
+ return Object.fromEntries(
206
+ paths.flatMap((path, i) => {
207
+ const last = target[`f${i}`]?.nodes[0];
208
+ return last ? [[path, new Date(last.committedDate)]] : [];
209
+ }),
210
+ );
211
+ }
212
+
213
+ // A file to save: text or bytes (null deletes it), and the blob id it had when loaded (null for a
214
+ // new file).
215
+ export type Change = { path: string; content: string | Uint8Array | null; oid: string | null };
216
+
217
+ // Saves files as one commit on the branch, through the Worker. Refuses (ConflictError) when one of them was changed or
218
+ // created elsewhere since it was loaded, so nobody's edit is overwritten. Returns the id of the new
219
+ // commit and the new blob ids.
220
+ export type Saved = { commit: string; blobs: Record<string, string> };
221
+
222
+ export async function commit(message: string, changes: Change[]): Promise<Saved> {
223
+ const saved = await api<Saved>('commit', {
224
+ message,
225
+ changes: changes.map((c) => ({
226
+ path: c.path,
227
+ oid: c.oid,
228
+ content: c.content === null ? null : base64(typeof c.content === 'string' ? new TextEncoder().encode(c.content) : c.content),
229
+ })),
230
+ });
231
+ // The texts saved are known by their new blob ids.
232
+ for (const c of changes) if (typeof c.content === 'string' && saved.blobs[c.path]) known.set(saved.blobs[c.path], c.content);
233
+ return saved;
234
+ }
235
+
236
+ function base64(bytes: Uint8Array): string {
237
+ let binary = '';
238
+ for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
239
+ return btoa(binary);
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------------------------------
243
+ // The history: earlier versions of a file, and files that were deleted (Versions.tsx, Deleted.tsx). Which commits
244
+ // changed a file is asked of GitHub; the folders and files in them come through the Worker's cache, like the files now.
245
+
246
+ /** A commit that changed a file: its id, its root tree (the files in it), when, and who saved it (their name on GitHub). */
247
+ export type Version = { commit: string; tree: string; at: Date; by: string };
248
+
249
+ type HistoryNode = {
250
+ oid: string;
251
+ tree: { oid: string };
252
+ committedDate: string;
253
+ author: { name: string | null; user: { name: string | null; login: string } | null };
254
+ };
255
+
256
+ const NODE = 'oid tree { oid } committedDate author { name user { name login } }';
257
+
258
+ const versionOf = (n: HistoryNode): Version => ({
259
+ commit: n.oid,
260
+ tree: n.tree.oid,
261
+ at: new Date(n.committedDate),
262
+ by: n.author.user?.name || n.author.name || n.author.user?.login || '',
263
+ });
264
+
265
+ /** The commits that changed a file, newest first, a page at a time (`after`: where the last ended). */
266
+ export async function versionsOf(path: string, after?: string): Promise<{ versions: Version[]; more: string | null }> {
267
+ const data = await graphql<{
268
+ repository: {
269
+ ref: { target: { history: { nodes: HistoryNode[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } };
270
+ };
271
+ }>(
272
+ `query($owner: String!, $name: String!, $path: String!, $after: String) { repository(owner: $owner, name: $name) {
273
+ ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
274
+ history(first: 20, path: $path, after: $after) {
275
+ nodes { ${NODE} }
276
+ pageInfo { hasNextPage endCursor }
277
+ }
278
+ } } }
279
+ } }`,
280
+ { owner: REPO.owner, name: REPO.name, path, after: after ?? null },
281
+ );
282
+ const { nodes, pageInfo } = data.repository.ref.target.history;
283
+ return { versions: nodes.map(versionOf), more: pageInfo.hasNextPage ? pageInfo.endCursor : null };
284
+ }
285
+
286
+ /** A file's text as it was in a version; null when it was not there (deleted in that commit). */
287
+ export async function versionText(version: Version, path: string): Promise<string | null> {
288
+ const [file] = await loadFiles([path], version.tree);
289
+ return file ? file.text : null;
290
+ }
291
+
292
+ /** A file that was deleted: its path, its last content (blob id, and text unless it is a picture),
293
+ * and when and by whom it was deleted. */
294
+ export type DeletedFile = { path: string; oid: string; text: string; at: Date; by: string };
295
+
296
+ type Entry = { name: string; oid: string; type: string; binary: boolean };
297
+
298
+ // The folders at paths under root trees (`path` '' the root itself), 200 to a request: their ids and what is in them
299
+ // (names, ids and kinds), or null where there is none.
300
+ async function foldersAt(at: { root: string; path: string }[]): Promise<({ oid: string; entries: Entry[] } | null)[]> {
301
+ const parts = [];
302
+ for (let i = 0; i < at.length; i += 200) parts.push(at.slice(i, i + 200));
303
+ const answers = await Promise.all(
304
+ parts.map((part) => api<{ trees: ({ oid: string; entries: Entry[] } | null)[] }>('trees', { at: part })),
305
+ );
306
+ return answers.flatMap((a) => a.trees);
307
+ }
308
+
309
+ /** The files deleted from a folder (and the folders in it) in its last `last` commits, newest first,
310
+ * each once, and not one that is there again. A file that went in a commit that also added one to the
311
+ * folder moved (a new address, a picture's new name) and is not counted. `text`: whether to get the
312
+ * files' text (not for pictures). */
313
+ export async function deletedFiles(folder: string, { last = 40, text = true } = {}): Promise<DeletedFile[]> {
314
+ const history = await graphql<{
315
+ repository: { ref: { target: { history: { nodes: (HistoryNode & { parents: { nodes: { tree: { oid: string } }[] } })[] } } } };
316
+ }>(
317
+ `query($owner: String!, $name: String!, $path: String!) { repository(owner: $owner, name: $name) {
318
+ ref(qualifiedName: ${JSON.stringify(`refs/heads/${REPO.branch}`)}) { target { ... on Commit {
319
+ history(first: ${last}, path: $path) {
320
+ nodes { ${NODE} parents(first: 1) { nodes { tree { oid } } } }
321
+ }
322
+ } } }
323
+ } }`,
324
+ { owner: REPO.owner, name: REPO.name, path: folder },
325
+ );
326
+ const commits = history.repository.ref.target.history.nodes;
327
+ if (!commits.length) return [];
328
+ // The folder in each commit and in the commit before it.
329
+ const sides = commits.flatMap((c) => [c.tree.oid, c.parents.nodes[0]?.tree.oid]);
330
+ const roots = await foldersAt(sides.flatMap((root) => (root ? [{ root, path: folder }] : [])));
331
+ const found = new Map<string, Entry[]>(roots.flatMap((r) => (r ? [[r.oid, r.entries] as const] : [])));
332
+ let next = 0;
333
+ const rootOid = sides.map((root) => (root ? (roots[next++]?.oid ?? null) : null));
334
+
335
+ // What each commit changed in the folder, a level of folders at a time: the trees that differ are
336
+ // compared again, the same ones left alone.
337
+ type Pair = { commit: number; path: string; now: string | null; was: string | null };
338
+ const removed: { commit: number; path: string; oid: string; binary: boolean }[] = [];
339
+ const added = new Set<number>();
340
+ let pairs: Pair[] = commits.map((_, i) => ({ commit: i, path: folder, now: rootOid[2 * i], was: rootOid[2 * i + 1] }));
341
+ pairs = pairs.filter((p) => p.now !== p.was);
342
+ while (pairs.length) {
343
+ const wanted = [...new Set(pairs.flatMap((p) => [p.now, p.was].filter((o): o is string => !!o && !found.has(o))))];
344
+ for (const t of await foldersAt(wanted.map((root) => ({ root, path: '' })))) if (t) found.set(t.oid, t.entries);
345
+ const next: Pair[] = [];
346
+ for (const p of pairs) {
347
+ const now = new Map((p.now ? (found.get(p.now) ?? []) : []).map((e) => [e.name, e]));
348
+ const was = new Map((p.was ? (found.get(p.was) ?? []) : []).map((e) => [e.name, e]));
349
+ for (const [name, e] of was) {
350
+ const there = now.get(name);
351
+ if (there?.oid === e.oid) continue;
352
+ const path = `${p.path}/${name}`;
353
+ if (e.type === 'tree') next.push({ commit: p.commit, path, now: there?.type === 'tree' ? there.oid : null, was: e.oid });
354
+ else if (!there) removed.push({ commit: p.commit, path, oid: e.oid, binary: e.binary });
355
+ }
356
+ for (const [name, e] of now) if (!was.has(name) && (e.type === 'blob' || e.type === 'tree')) added.add(p.commit);
357
+ }
358
+ pairs = next;
359
+ }
360
+
361
+ // The newest deletion of each file (the commits are newest first), not one that moved.
362
+ const seen = new Set<string>();
363
+ const gone = removed.sort((a, b) => a.commit - b.commit).filter((r) => !added.has(r.commit) && !seen.has(r.path) && seen.add(r.path));
364
+ if (!gone.length) return [];
365
+ // Still there (put back since, or another file under the same name): not deleted.
366
+ const now = (await listFiles({ paths: gone.map((r) => r.path) })).paths;
367
+ const deleted = gone.filter((_, i) => !now[i]);
368
+ if (!deleted.length) return [];
369
+ const files = text
370
+ ? await withTexts(deleted.map((r) => ({ path: r.path, oid: r.oid, binary: r.binary })))
371
+ : deleted.map((r) => ({ text: '' }));
372
+ return deleted.map((r, i) => {
373
+ const { at, by } = versionOf(commits[r.commit]);
374
+ return { path: r.path, oid: r.oid, text: files[i].text, at, by };
375
+ });
376
+ }