@morphemecms/cms 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/astro.mjs +3 -0
  3. package/bin/morpheme.mjs +3 -1
  4. package/defaults/blockTexts.ts +2 -2
  5. package/defaults/collections.ts +10 -0
  6. package/defaults/editor.ts +57 -53
  7. package/defaults/privacy.ts +4 -1
  8. package/defaults/rules.ts +36 -38
  9. package/defaults/standardPages.ts +0 -3
  10. package/defaults/texts.ts +11 -2
  11. package/demo/src/content/pages/about.md +2 -0
  12. package/icons/lucide/browser-fill.svg +1 -0
  13. package/icons/lucide/buildings-fill.svg +1 -0
  14. package/icons/lucide/certificate-fill.svg +1 -0
  15. package/icons/lucide/chart-line-up-fill.svg +1 -0
  16. package/icons/lucide/chats-fill.svg +1 -0
  17. package/icons/lucide/envelope-simple-fill.svg +1 -0
  18. package/icons/lucide/file-text-fill.svg +1 -0
  19. package/icons/lucide/folder-simple-fill.svg +1 -0
  20. package/icons/lucide/gauge.svg +1 -1
  21. package/icons/lucide/gear-six.svg +1 -0
  22. package/icons/lucide/globe-simple.svg +1 -0
  23. package/icons/lucide/heart-fill.svg +1 -0
  24. package/icons/lucide/layout.svg +1 -0
  25. package/icons/lucide/list-checks-fill.svg +1 -0
  26. package/icons/lucide/magnifying-glass-fill.svg +1 -0
  27. package/icons/lucide/megaphone-fill.svg +1 -0
  28. package/icons/lucide/newspaper-fill.svg +1 -0
  29. package/icons/lucide/paint-brush-broad-fill.svg +1 -0
  30. package/icons/lucide/puzzle-piece-fill.svg +1 -0
  31. package/icons/lucide/shield-check-fill.svg +1 -0
  32. package/icons/lucide/sidebar-simple.svg +1 -1
  33. package/icons/lucide/sidebar.svg +1 -0
  34. package/icons/lucide/square-half-bottom-fill.svg +1 -0
  35. package/icons/lucide/square-half.svg +1 -0
  36. package/icons/lucide/squares-four-fill.svg +1 -0
  37. package/icons/lucide/storefront-fill.svg +1 -0
  38. package/icons/lucide/tag-fill.svg +1 -0
  39. package/icons/lucide/toggle-right-fill.svg +1 -0
  40. package/icons/lucide/trash-fill.svg +1 -0
  41. package/icons/lucide/user-fill.svg +1 -0
  42. package/icons/lucide/wrench-fill.svg +1 -0
  43. package/icons/material-symbols/browser-fill.svg +1 -0
  44. package/icons/material-symbols/buildings-fill.svg +1 -0
  45. package/icons/material-symbols/certificate-fill.svg +1 -0
  46. package/icons/material-symbols/chart-line-up-fill.svg +1 -0
  47. package/icons/material-symbols/chats-fill.svg +1 -0
  48. package/icons/material-symbols/envelope-simple-fill.svg +1 -0
  49. package/icons/material-symbols/file-text-fill.svg +1 -0
  50. package/icons/material-symbols/folder-simple-fill.svg +1 -0
  51. package/icons/material-symbols/gauge.svg +1 -1
  52. package/icons/material-symbols/gear-six.svg +1 -0
  53. package/icons/material-symbols/globe-simple.svg +1 -0
  54. package/icons/material-symbols/heart-fill.svg +1 -0
  55. package/icons/material-symbols/layout.svg +1 -0
  56. package/icons/material-symbols/list-checks-fill.svg +1 -0
  57. package/icons/material-symbols/magnifying-glass-fill.svg +1 -0
  58. package/icons/material-symbols/megaphone-fill.svg +1 -0
  59. package/icons/material-symbols/newspaper-fill.svg +1 -0
  60. package/icons/material-symbols/paint-brush-broad-fill.svg +1 -0
  61. package/icons/material-symbols/puzzle-piece-fill.svg +1 -0
  62. package/icons/material-symbols/shield-check-fill.svg +1 -0
  63. package/icons/material-symbols/sidebar-simple.svg +1 -1
  64. package/icons/material-symbols/sidebar.svg +1 -0
  65. package/icons/material-symbols/square-half-bottom-fill.svg +1 -0
  66. package/icons/material-symbols/square-half.svg +1 -0
  67. package/icons/material-symbols/squares-four-fill.svg +1 -0
  68. package/icons/material-symbols/storefront-fill.svg +1 -0
  69. package/icons/material-symbols/tag-fill.svg +1 -0
  70. package/icons/material-symbols/toggle-right-fill.svg +1 -0
  71. package/icons/material-symbols/trash-fill.svg +1 -0
  72. package/icons/material-symbols/user-fill.svg +1 -0
  73. package/icons/material-symbols/wrench-fill.svg +1 -0
  74. package/icons/phosphor/browser-fill.svg +1 -0
  75. package/icons/phosphor/buildings-fill.svg +1 -0
  76. package/icons/phosphor/certificate-fill.svg +1 -0
  77. package/icons/phosphor/chart-line-up-fill.svg +1 -0
  78. package/icons/phosphor/chats-fill.svg +1 -0
  79. package/icons/phosphor/envelope-simple-fill.svg +1 -0
  80. package/icons/phosphor/file-text-fill.svg +1 -0
  81. package/icons/phosphor/folder-simple-fill.svg +1 -0
  82. package/icons/phosphor/gear-six.svg +1 -0
  83. package/icons/phosphor/globe-simple.svg +1 -0
  84. package/icons/phosphor/heart-fill.svg +1 -0
  85. package/icons/phosphor/layout.svg +1 -0
  86. package/icons/phosphor/list-checks-fill.svg +1 -0
  87. package/icons/phosphor/magnifying-glass-fill.svg +1 -0
  88. package/icons/phosphor/megaphone-fill.svg +1 -0
  89. package/icons/phosphor/newspaper-fill.svg +1 -0
  90. package/icons/phosphor/paint-brush-broad-fill.svg +1 -0
  91. package/icons/phosphor/puzzle-piece-fill.svg +1 -0
  92. package/icons/phosphor/shield-check-fill.svg +1 -0
  93. package/icons/phosphor/sidebar.svg +1 -0
  94. package/icons/phosphor/square-half-bottom-fill.svg +1 -0
  95. package/icons/phosphor/square-half.svg +1 -0
  96. package/icons/phosphor/squares-four-fill.svg +1 -0
  97. package/icons/phosphor/storefront-fill.svg +1 -0
  98. package/icons/phosphor/tag-fill.svg +1 -0
  99. package/icons/phosphor/toggle-right-fill.svg +1 -0
  100. package/icons/phosphor/trash-fill.svg +1 -0
  101. package/icons/phosphor/user-fill.svg +1 -0
  102. package/icons/phosphor/wrench-fill.svg +1 -0
  103. package/lib/breakpoints.d.mts +13 -0
  104. package/lib/breakpoints.mjs +20 -0
  105. package/lib/breakpoints.test.mjs +39 -0
  106. package/lib/build-lane.d.mts +15 -0
  107. package/lib/build-lane.mjs +23 -0
  108. package/lib/build-lane.test.mjs +74 -0
  109. package/lib/build-settings.d.mts +16 -0
  110. package/lib/build-settings.mjs +39 -0
  111. package/lib/build-settings.test.mjs +41 -0
  112. package/lib/contracts.d.mts +3 -2
  113. package/lib/contracts.mjs +3 -2
  114. package/lib/drawer-swipe.d.mts +10 -0
  115. package/lib/drawer-swipe.mjs +17 -0
  116. package/lib/drawer-swipe.test.mjs +33 -0
  117. package/lib/drawerKeys.d.mts +9 -0
  118. package/lib/drawerKeys.mjs +26 -0
  119. package/lib/drawerKeys.test.mjs +21 -0
  120. package/lib/eyebrow.d.mts +6 -0
  121. package/lib/eyebrow.mjs +24 -0
  122. package/lib/eyebrow.test.mjs +20 -0
  123. package/lib/front-sections.test.mjs +82 -0
  124. package/lib/heading-levels.d.mts +7 -0
  125. package/lib/heading-levels.mjs +16 -0
  126. package/lib/heading-levels.test.mjs +37 -0
  127. package/lib/inline.ts +3 -2
  128. package/lib/markdown.ts +2 -0
  129. package/lib/materialIcon.d.mts +21 -0
  130. package/lib/materialIcon.mjs +29 -0
  131. package/lib/materialIcon.test.mjs +45 -0
  132. package/lib/menu-layout.d.mts +11 -0
  133. package/lib/menu-layout.mjs +14 -0
  134. package/lib/menu-layout.test.mjs +23 -0
  135. package/lib/menu.d.mts +59 -0
  136. package/lib/menu.mjs +129 -0
  137. package/lib/menu.test.mjs +291 -0
  138. package/lib/rehype-emphasize.d.mts +4 -0
  139. package/lib/rehype-emphasize.mjs +20 -0
  140. package/lib/rehype-emphasize.test.mjs +39 -0
  141. package/lib/section-in-view.d.mts +8 -0
  142. package/lib/section-in-view.mjs +25 -0
  143. package/lib/section-in-view.test.mjs +35 -0
  144. package/lib/section-marker.d.mts +5 -0
  145. package/lib/section-marker.mjs +18 -0
  146. package/lib/section-marker.test.mjs +22 -0
  147. package/lib/section-ranges.d.mts +12 -0
  148. package/lib/section-ranges.mjs +43 -0
  149. package/lib/section-ranges.test.mjs +61 -0
  150. package/lib/sheet-stops.d.mts +68 -0
  151. package/lib/sheet-stops.mjs +198 -0
  152. package/lib/sheet-stops.test.mjs +155 -0
  153. package/lib/still-timer.d.mts +5 -0
  154. package/lib/still-timer.mjs +17 -0
  155. package/lib/still-timer.test.mjs +42 -0
  156. package/lib/text-sections.d.mts +10 -0
  157. package/lib/text-sections.mjs +45 -0
  158. package/lib/text-sections.test.mjs +78 -0
  159. package/lib/video.mjs +2 -1
  160. package/lib/without-h1.d.mts +1 -0
  161. package/lib/without-h1.mjs +7 -0
  162. package/lib/without-h1.test.mjs +26 -0
  163. package/lib/word-at.d.mts +6 -0
  164. package/lib/word-at.mjs +20 -0
  165. package/lib/word-at.test.mjs +19 -0
  166. package/package.json +3 -1
  167. package/pages/editor/version.json.js +22 -2
  168. package/scripts/already-checked.mjs +11 -0
  169. package/scripts/already-checked.test.mjs +18 -0
  170. package/scripts/build-settings-problems.mjs +27 -0
  171. package/scripts/build-settings-problems.test.mjs +35 -0
  172. package/scripts/check-content.mjs +42 -4
  173. package/scripts/check-worker.mjs +96 -4
  174. package/scripts/ci-build.mjs +85 -0
  175. package/scripts/ci-build.test.mjs +137 -0
  176. package/scripts/editor-roundtrip.mjs +62 -3
  177. package/scripts/editor-screens.mjs +21 -7
  178. package/scripts/git-env.mjs +6 -0
  179. package/scripts/step-command.mjs +34 -0
  180. package/scripts/step-command.test.mjs +50 -0
  181. package/scripts/text-problems.mjs +83 -0
  182. package/scripts/text-problems.test.mjs +74 -0
  183. package/scripts/worker-modules.mjs +11 -1
  184. package/site/breakpoints.d.mts +6 -0
  185. package/site/breakpoints.mjs +52 -0
  186. package/site/breakpoints.test.mjs +36 -0
  187. package/site/casing.d.mts +3 -0
  188. package/site/casing.mjs +26 -0
  189. package/site/content.config.ts +2 -0
  190. package/site/content.ts +2 -4
  191. package/site/defineSite.d.mts +1 -0
  192. package/site/defineSite.mjs +3 -0
  193. package/site/home.ts +3 -2
  194. package/site/icons.d.mts +116 -4
  195. package/site/icons.mjs +62 -6
  196. package/site/images.mjs +1 -1
  197. package/site/live-diff.d.mts +22 -0
  198. package/site/live-diff.mjs +98 -0
  199. package/site/live-diff.test.mjs +235 -0
  200. package/site/materialSymbols.d.mts +5 -0
  201. package/site/materialSymbols.mjs +19 -0
  202. package/site/sections.ts +1 -1
  203. package/site/theme.d.mts +1 -1
  204. package/site/theme.mjs +7 -1
  205. package/src/ActionParts.tsx +1 -1
  206. package/src/Advanced.tsx +101 -20
  207. package/src/App.tsx +47 -121
  208. package/src/AppList.tsx +1 -1
  209. package/src/BlockParts.tsx +3 -3
  210. package/src/Colors.tsx +1 -1
  211. package/src/CommunicationParts.tsx +1 -1
  212. package/src/ContainmentParts.tsx +80 -28
  213. package/src/Deleted.tsx +18 -4
  214. package/src/EditorFooter.tsx +14 -0
  215. package/src/EntryForm.tsx +75 -39
  216. package/src/FormFrame.tsx +3 -8
  217. package/src/Library.tsx +46 -51
  218. package/src/Licenses.tsx +27 -11
  219. package/src/MainDrawerParts.tsx +60 -0
  220. package/src/MainMenu.tsx +136 -0
  221. package/src/MarkdownEditor.tsx +175 -32
  222. package/src/Media.tsx +69 -16
  223. package/src/MediaTags.tsx +91 -0
  224. package/src/ModalSheetSample.tsx +47 -0
  225. package/src/NavigationParts.tsx +67 -21
  226. package/src/NavigationRestParts.tsx +1 -1
  227. package/src/Parts.tsx +71 -0
  228. package/src/PartsShown.tsx +1 -1
  229. package/src/PickerParts.tsx +2 -2
  230. package/src/Redirects.tsx +3 -2
  231. package/src/Screen.tsx +93 -90
  232. package/src/SelectionParts.tsx +22 -6
  233. package/src/SiteLogoLink.tsx +14 -0
  234. package/src/StartFab.tsx +22 -0
  235. package/src/StartStatus.tsx +1 -1
  236. package/src/autoTags.d.mts +28 -0
  237. package/src/autoTags.mjs +68 -0
  238. package/src/autoTags.test.mjs +109 -0
  239. package/src/buildSettings.test.mjs +20 -0
  240. package/src/buildSettings.ts +20 -0
  241. package/src/checkSite.ts +14 -1
  242. package/src/deploy.ts +7 -9
  243. package/src/editor.css +150 -17
  244. package/src/eyebrow.d.mts +5 -0
  245. package/src/eyebrow.mjs +53 -0
  246. package/src/eyebrowState.d.mts +21 -0
  247. package/src/eyebrowState.mjs +62 -0
  248. package/src/eyebrowState.test.mjs +78 -0
  249. package/src/fileRecords.d.mts +115 -0
  250. package/src/fileRecords.mjs +224 -0
  251. package/src/fileRecords.test.mjs +224 -0
  252. package/src/fileTagFilter.d.mts +30 -0
  253. package/src/fileTagFilter.mjs +44 -0
  254. package/src/fileTagFilter.test.mjs +57 -0
  255. package/src/fileTags.d.mts +56 -0
  256. package/src/fileTags.mjs +100 -0
  257. package/src/fileTags.test.mjs +127 -0
  258. package/src/firstHeading.d.mts +6 -0
  259. package/src/firstHeading.mjs +37 -0
  260. package/src/i18n/da-DK/advanced.ts +27 -12
  261. package/src/i18n/da-DK/app.ts +9 -7
  262. package/src/i18n/da-DK/apps.ts +16 -16
  263. package/src/i18n/da-DK/blockSettings.ts +9 -9
  264. package/src/i18n/da-DK/blocks.ts +2 -2
  265. package/src/i18n/da-DK/check.ts +5 -2
  266. package/src/i18n/da-DK/cms.ts +14 -12
  267. package/src/i18n/da-DK/colors.ts +5 -5
  268. package/src/i18n/da-DK/common.ts +2 -2
  269. package/src/i18n/da-DK/company.ts +3 -3
  270. package/src/i18n/da-DK/consents.ts +6 -6
  271. package/src/i18n/da-DK/forms.ts +6 -6
  272. package/src/i18n/da-DK/history.ts +6 -6
  273. package/src/i18n/da-DK/keywords.ts +2 -2
  274. package/src/i18n/da-DK/library.ts +17 -15
  275. package/src/i18n/da-DK/licenses.ts +4 -4
  276. package/src/i18n/da-DK/media.ts +10 -4
  277. package/src/i18n/da-DK/parts.ts +62 -25
  278. package/src/i18n/da-DK/preview.ts +5 -6
  279. package/src/i18n/da-DK/redirects.ts +7 -7
  280. package/src/i18n/da-DK/seo.ts +1 -1
  281. package/src/i18n/da-DK/text.ts +2 -0
  282. package/src/i18n/da-DK/theme.ts +5 -5
  283. package/src/i18n/en-US/advanced.ts +34 -18
  284. package/src/i18n/en-US/app.ts +7 -5
  285. package/src/i18n/en-US/apps.ts +9 -9
  286. package/src/i18n/en-US/blockSettings.ts +4 -4
  287. package/src/i18n/en-US/blocks.ts +6 -6
  288. package/src/i18n/en-US/check.ts +6 -3
  289. package/src/i18n/en-US/cms.ts +15 -14
  290. package/src/i18n/en-US/colors.ts +18 -18
  291. package/src/i18n/en-US/common.ts +3 -3
  292. package/src/i18n/en-US/consents.ts +4 -4
  293. package/src/i18n/en-US/forms.ts +5 -5
  294. package/src/i18n/en-US/history.ts +2 -2
  295. package/src/i18n/en-US/library.ts +21 -15
  296. package/src/i18n/en-US/licenses.ts +6 -6
  297. package/src/i18n/en-US/media.ts +7 -1
  298. package/src/i18n/en-US/parts.ts +60 -21
  299. package/src/i18n/en-US/preview.ts +5 -6
  300. package/src/i18n/en-US/redirects.ts +3 -3
  301. package/src/i18n/en-US/seo.ts +1 -1
  302. package/src/i18n/en-US/text.ts +3 -0
  303. package/src/i18n/en-US/theme.ts +6 -6
  304. package/src/icon.tsx +90 -27
  305. package/src/licenseData.test.mjs +28 -0
  306. package/src/licenseData.ts +12 -1
  307. package/src/markdown.d.mts +2 -0
  308. package/src/markdown.mjs +73 -5
  309. package/src/markdown.test.mjs +74 -0
  310. package/src/materialIcons.ts +8 -0
  311. package/src/menuChildren.ts +36 -0
  312. package/src/menuOpen.ts +34 -0
  313. package/src/pagePreview.tsx +23 -19
  314. package/src/r2Caching.ts +14 -0
  315. package/src/sectionFrame.ts +39 -0
  316. package/src/sectionState.d.mts +19 -0
  317. package/src/sectionState.mjs +31 -0
  318. package/src/sectionState.test.mjs +59 -0
  319. package/src/sections.d.mts +9 -7
  320. package/src/sections.mjs +74 -27
  321. package/src/sections.test.mjs +59 -0
  322. package/src/sidebar.ts +2 -2
  323. package/src/site.ts +27 -17
  324. package/src/stillCursor.d.mts +8 -0
  325. package/src/stillCursor.mjs +19 -0
  326. package/src/stillCursor.test.mjs +26 -0
  327. package/src/tagUses.d.mts +47 -0
  328. package/src/tagUses.mjs +121 -0
  329. package/src/tagUses.test.mjs +141 -0
  330. package/src/themeUi.ts +10 -0
  331. package/src/topBarSlot.tsx +37 -0
  332. package/src/ui.tsx +654 -150
  333. package/src/virtual.d.ts +5 -0
  334. package/starter/README.md +2 -1
  335. package/starter/package.json +2 -1
  336. package/starter/wrangler.jsonc +3 -2
  337. package/theme/blocks/cards/LinkCards.tsx +2 -1
  338. package/theme/blocks/hero/View.tsx +2 -1
  339. package/theme/blocks/video/View.tsx +2 -1
  340. package/theme/components/BlogIndex.astro +2 -2
  341. package/theme/components/BlogPosts.astro +1 -1
  342. package/theme/components/BlogPosts.tsx +9 -6
  343. package/theme/components/Container.astro +1 -1
  344. package/theme/components/CookieConsent.astro +1 -1
  345. package/theme/components/EndBlock.astro +1 -1
  346. package/theme/components/Footer.astro +5 -5
  347. package/theme/components/Header.astro +3 -3
  348. package/theme/components/Icon.astro +2 -2
  349. package/theme/components/MobileMenu.tsx +72 -61
  350. package/theme/components/MobileNav.astro +2 -2
  351. package/theme/components/PageHeader.astro +2 -2
  352. package/theme/components/PageSection.astro +1 -1
  353. package/theme/components/PopupButton.tsx +2 -2
  354. package/theme/components/PopupView.tsx +2 -2
  355. package/theme/components/PostList.tsx +7 -3
  356. package/theme/components/RichText.astro +22 -7
  357. package/theme/components/Section.astro +17 -10
  358. package/theme/components/SectionHeading.tsx +1 -1
  359. package/theme/components/home/PageBlock.astro +4 -4
  360. package/theme/components/home/Text.astro +3 -3
  361. package/theme/layouts/PageLayout.astro +3 -3
  362. package/theme/layouts/PostLayout.astro +9 -8
  363. package/theme/styles/base.css +13 -3
  364. package/theme/styles/blocks.css +97 -10
  365. package/theme/styles/components.css +9 -9
  366. package/theme/styles/elevation.test.mjs +168 -0
  367. package/theme/styles/global.css +65 -59
  368. package/theme/styles/ui.css +460 -131
  369. package/theme/templates/Home.astro +1 -1
  370. package/theme/templates/New.astro +5 -5
  371. package/theme/templates/NotFound.astro +2 -2
  372. package/theme/ui/BottomSheet.tsx +118 -21
  373. package/theme/ui/BottomSheetResizeIcon.tsx +13 -0
  374. package/theme/ui/Button.tsx +4 -3
  375. package/theme/ui/Dialog.tsx +1 -1
  376. package/theme/ui/FabMenu.tsx +13 -12
  377. package/theme/ui/LoadingIndicator.tsx +1 -1
  378. package/theme/ui/NavigationDrawer.tsx +116 -15
  379. package/theme/ui/NavigationRail.tsx +22 -13
  380. package/theme/ui/Search.tsx +1 -1
  381. package/theme/ui/SettingRow.tsx +31 -0
  382. package/theme/ui/Switch.tsx +11 -11
  383. package/theme/ui/TextField.tsx +17 -6
  384. package/theme/ui/bottomSheet.script.ts +3 -2
  385. package/theme/ui/bottomSheetControl.ts +131 -46
  386. package/theme/ui/fabMenu.script.ts +69 -53
  387. package/theme/ui/modalSheetSize.ts +214 -0
  388. package/theme/ui/navigationDrawerControl.ts +35 -0
  389. package/theme/ui/search.script.ts +4 -2
  390. package/worker/index.js +3 -0
  391. package/worker/plan-status.js +46 -0
  392. package/worker/push.js +7 -5
  393. package/worker/r2-status.js +2 -1
  394. package/icons/lucide/desktop.svg +0 -1
  395. package/icons/material-symbols/desktop.svg +0 -1
  396. package/icons/phosphor/desktop.svg +0 -1
@@ -0,0 +1,214 @@
1
+ // How high a modal bottom sheet is on a phone and how it is pulled (bottomSheet() in bottomSheetControl.ts uses it):
2
+ // modalSheetSize(dialog, panel, close) opens it at its start height (BottomSheet's `startAt`, data-start on the container,
3
+ // 'half' without it: modalStartHeight in lib/sheet-stops.mjs), lets it be pulled by its handle, its header or its content,
4
+ // and gives it up to close() when it is let go low enough (modalRestAfterDrag). While it is managed the container has
5
+ // data-sized, data-full at full height and data-moving while it is pulled or moves (ui.css): the content scrolls, and a
6
+ // frame in it takes touches, only at full height; below that a touch on the content pulls the sheet. At full height the
7
+ // content pulls the sheet only when it is scrolled to the top and moves down. Touch only for the content (a mouse has no
8
+ // scrolling to tell apart); pointer events for the handle and the header. The editor's sheets (bottom-sheet-fit and
9
+ // bottom-sheet-full on the dialog) and the medium breakpoint upwards are left alone. The height follows the site's
10
+ // motion scheme (motionSpring); with reduced motion it changes at once. Its window and media query listeners remove
11
+ // themselves once the dialog is no longer in the page.
12
+ import { animate } from 'motion';
13
+ import { atLeast } from '@morphemecms/cms/lib/breakpoints.mjs';
14
+ import { motionSpring } from '@morphemecms/cms/lib/motion';
15
+ import { modalRestAfterDrag, modalStartHeight } from '@morphemecms/cms/lib/sheet-stops.mjs';
16
+
17
+ export type ModalSheetSize = {
18
+ /** Whether it manages the sheet now: not the editor's sheets, and not from the medium breakpoint. */
19
+ managed: () => boolean;
20
+ /** Called when the dialog has been opened: measures and gives it its start height. */
21
+ open: () => void;
22
+ /** Called when it is closed: back to what ui.css says. */
23
+ reset: () => void;
24
+ };
25
+
26
+ export function modalSheetSize(dialog: HTMLDialogElement, panel: HTMLElement, close: () => void): ModalSheetSize {
27
+ const content = panel.querySelector<HTMLElement>('.bottom-sheet-content');
28
+ const handle = panel.querySelector<HTMLElement>('[data-bottom-sheet-handle]');
29
+ const header = panel.querySelector<HTMLElement>('.bottom-sheet-header');
30
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
31
+ const wide = matchMedia(atLeast('medium'));
32
+ // The editor's sheets keep the heights their classes give them.
33
+ const own = dialog.classList.contains('bottom-sheet-fit') || dialog.classList.contains('bottom-sheet-full');
34
+ const managed = () => !own && !wide.matches;
35
+
36
+ let sizes: { start: number; full: number } | undefined;
37
+ let anim: { stop: () => void } | undefined;
38
+ // Bumped whenever something takes over, so a finished animation does not settle over it.
39
+ let run = 0;
40
+
41
+ // The start and the full height: what is in it (a frame fills the sheet, so it counts as unlimited), the screen's
42
+ // height and --bottom-sheet-top-gap.
43
+ function measure() {
44
+ const was = { height: dialog.style.height, maxHeight: dialog.style.maxHeight };
45
+ dialog.style.height = 'auto';
46
+ dialog.style.maxHeight = 'none';
47
+ const natural = content?.querySelector('iframe') ? Infinity : panel.offsetHeight;
48
+ dialog.style.height = was.height;
49
+ dialog.style.maxHeight = was.maxHeight;
50
+ const probe = document.createElement('div');
51
+ probe.style.cssText = 'position:absolute;visibility:hidden;height:var(--bottom-sheet-top-gap, 4.5rem)';
52
+ dialog.append(probe);
53
+ const topGap = probe.offsetHeight;
54
+ probe.remove();
55
+ const startAt = panel.dataset.start === 'full' ? 'full' : 'half';
56
+ const full = modalStartHeight(natural, innerHeight, 'full', topGap);
57
+ return { start: Math.min(full, modalStartHeight(natural, innerHeight, startAt, topGap)), full };
58
+ }
59
+
60
+ // The sheet is this high now (and data-full says whether that is full height).
61
+ function settle(px: number) {
62
+ dialog.style.height = `${px}px`;
63
+ panel.toggleAttribute('data-full', !!sizes && px >= sizes.full - 1);
64
+ delete panel.dataset.moving;
65
+ }
66
+
67
+ async function go(px: number) {
68
+ const id = ++run;
69
+ anim?.stop();
70
+ const from = dialog.offsetHeight;
71
+ if (reduce || from === px) return settle(px);
72
+ panel.dataset.moving = '';
73
+ const animation = animate(dialog, { height: [from, px] }, motionSpring('spatial'));
74
+ anim = animation;
75
+ try {
76
+ await animation;
77
+ } catch {
78
+ return;
79
+ }
80
+ if (id === run) settle(px);
81
+ }
82
+
83
+ function open() {
84
+ if (!managed()) return;
85
+ sizes = measure();
86
+ panel.setAttribute('data-sized', '');
87
+ run++;
88
+ settle(sizes.start);
89
+ }
90
+
91
+ function reset() {
92
+ run++;
93
+ anim?.stop();
94
+ drag = pending = undefined;
95
+ sizes = undefined;
96
+ for (const name of ['data-sized', 'data-full', 'data-moving']) panel.removeAttribute(name);
97
+ dialog.style.height = '';
98
+ panel.style.transform = '';
99
+ }
100
+
101
+ // Pulling: from `h`, the height it had when the pull began, `raw` is where the finger would take it; between the start
102
+ // and the full height the height follows, below the start the sheet slides down instead (so the content keeps its
103
+ // layout), and above full it stays.
104
+ let pending: { y: number } | undefined;
105
+ let drag: { y: number; h: number; raw: number; last: { y: number; t: number }; v: number } | undefined;
106
+
107
+ function begin(y: number, t: number) {
108
+ run++;
109
+ anim?.stop();
110
+ drag = { y, h: dialog.offsetHeight, raw: dialog.offsetHeight, last: { y, t }, v: 0 };
111
+ pending = undefined;
112
+ panel.removeAttribute('data-full');
113
+ panel.dataset.moving = '';
114
+ }
115
+
116
+ function move(y: number, t: number) {
117
+ if (!drag || !sizes) return;
118
+ drag.raw = Math.min(sizes.full, drag.h - (y - drag.y));
119
+ const low = Math.min(sizes.start, drag.h);
120
+ dialog.style.height = `${Math.max(low, drag.raw)}px`;
121
+ panel.style.transform = drag.raw < low ? `translateY(${low - drag.raw}px)` : '';
122
+ const dt = t - drag.last.t;
123
+ if (dt > 0) drag.v = (drag.last.y - y) / dt;
124
+ drag.last = { y, t };
125
+ }
126
+
127
+ function end() {
128
+ pending = undefined;
129
+ if (!drag || !sizes) return;
130
+ const { raw, v } = drag;
131
+ drag = undefined;
132
+ const rest = modalRestAfterDrag(sizes.start, sizes.full, raw, v);
133
+ if (rest === 'close') {
134
+ delete panel.dataset.moving;
135
+ close();
136
+ return;
137
+ }
138
+ if (reduce) panel.style.transform = '';
139
+ else animate(panel, { y: 0 }, motionSpring('spatial'));
140
+ go(rest === 'full' ? sizes.full : sizes.start);
141
+ }
142
+
143
+ // The handle and the header: a pull after 4px (not by a button in the header).
144
+ for (const el of [handle, header]) {
145
+ el?.addEventListener('pointerdown', (e) => {
146
+ const target = e.target as Element;
147
+ if (!managed() || !sizes || e.button !== 0 || target.closest('button, a')) return;
148
+ pending = { y: e.clientY };
149
+ el.setPointerCapture(e.pointerId);
150
+ });
151
+ el?.addEventListener('pointermove', (e) => {
152
+ if (!pending && !drag) return;
153
+ if (!drag) {
154
+ if (Math.abs(e.clientY - pending!.y) < 4) return;
155
+ begin(e.clientY, e.timeStamp);
156
+ }
157
+ move(e.clientY, e.timeStamp);
158
+ });
159
+ el?.addEventListener('pointerup', end);
160
+ el?.addEventListener('pointercancel', end);
161
+ }
162
+
163
+ // The content: below full height every touch pulls the sheet (ui.css has stopped it scrolling); at full height a touch
164
+ // pulls it only when it moves down while the content is at the top, else the content scrolls.
165
+ content?.addEventListener('touchstart', (e) => {
166
+ if (managed() && sizes && e.touches.length === 1) pending = { y: e.touches[0].clientY };
167
+ });
168
+ content?.addEventListener(
169
+ 'touchmove',
170
+ (e) => {
171
+ if (!pending && !drag) return;
172
+ const y = e.touches[0].clientY;
173
+ if (!drag) {
174
+ const dy = y - pending!.y;
175
+ if (panel.hasAttribute('data-full')) {
176
+ if (Math.abs(dy) < 4) return;
177
+ if (dy < 0 || content.scrollTop > 0) {
178
+ pending = undefined;
179
+ return;
180
+ }
181
+ } else if (Math.abs(dy) < 4) return;
182
+ begin(y, e.timeStamp);
183
+ }
184
+ if (e.cancelable) e.preventDefault();
185
+ move(y, e.timeStamp);
186
+ },
187
+ { passive: false },
188
+ );
189
+ content?.addEventListener('touchend', end);
190
+ content?.addEventListener('touchcancel', end);
191
+
192
+ // Becoming a dialog (from the medium breakpoint) or a phone again, and a phone turned: measured again. Both listeners
193
+ // remove themselves once the dialog is no longer in the page (the gallery's sample mounts and unmounts it).
194
+ const onWide = () => {
195
+ if (!dialog.isConnected) return gone();
196
+ if (wide.matches) reset();
197
+ else if (dialog.open) open();
198
+ };
199
+ const onResize = () => {
200
+ if (!dialog.isConnected) return gone();
201
+ if (!dialog.open || !managed() || !sizes || drag) return;
202
+ const full = panel.hasAttribute('data-full');
203
+ sizes = measure();
204
+ settle(full ? sizes.full : sizes.start);
205
+ };
206
+ function gone() {
207
+ wide.removeEventListener('change', onWide);
208
+ removeEventListener('resize', onResize);
209
+ }
210
+ wide.addEventListener('change', onWide);
211
+ addEventListener('resize', onResize);
212
+
213
+ return { managed, open, reset };
214
+ }
@@ -0,0 +1,35 @@
1
+ // What a navigation drawer (NavigationDrawer.tsx) does in a browser: the arrow keys, Home and End move the focus between
2
+ // its items and sub-items (not its toggles, which stay on Tab), and a modal one puts the focus on its first item when it
3
+ // opens. Exposes handleDrawerKey, navigationDrawerKeys and focusNavigationDrawerItem; where the focus goes is
4
+ // lib/drawerKeys.mjs (tested).
5
+ import { drawerKeyTarget } from '@morphemecms/cms/lib/drawerKeys.mjs';
6
+
7
+ const itemSelector = '.navigation-drawer-item, .navigation-drawer-subitem';
8
+
9
+ /** The items in the drawer that can be focused now, in the order they stand. */
10
+ const itemsOf = (root: HTMLElement) =>
11
+ [...root.querySelectorAll<HTMLElement>(itemSelector)].filter(
12
+ (item) => !item.hidden && !item.hasAttribute('disabled') && item.getClientRects().length > 0,
13
+ );
14
+
15
+ /** Moves the focus for the arrow keys, Home and End when it is on an item of `root`. */
16
+ export function handleDrawerKey(event: KeyboardEvent, root: HTMLElement) {
17
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
18
+ const items = itemsOf(root);
19
+ const target = drawerKeyTarget(event.key, items.indexOf(document.activeElement as HTMLElement), items.length);
20
+ if (target === null) return;
21
+ event.preventDefault();
22
+ items[target]?.focus();
23
+ }
24
+
25
+ /** Listens for the keys on `root` (a drawer drawn as a class on an element of its own: the site's menu); returns what removes it. */
26
+ export function navigationDrawerKeys(root: HTMLElement): () => void {
27
+ const onKeyDown = (event: KeyboardEvent) => handleDrawerKey(event, root);
28
+ root.addEventListener('keydown', onKeyDown);
29
+ return () => root.removeEventListener('keydown', onKeyDown);
30
+ }
31
+
32
+ /** The focus on the drawer's first item (a modal one, when it opens). */
33
+ export function focusNavigationDrawerItem(root: HTMLElement) {
34
+ itemsOf(root)[0]?.focus();
35
+ }
@@ -1,14 +1,16 @@
1
1
  // What a search (Search.tsx) does on the site: its bar opens the search view (a modal <dialog>: over the whole screen
2
- // on a phone, docked where the bar is from 48rem, .search-view-docked with --search-view-top, -left and -width), with the
2
+ // on a phone, docked where the bar is from the medium breakpoint, .search-view-docked with --search-view-top, -left and -width), with the
3
3
  // focus in its field; the back button, Escape and a tap outside close it, and the focus goes back to the bar. What is
4
4
  // written narrows the results to those whose text has it (the message .search-view-empty when none has), the arrow keys
5
5
  // move through them (aria-activedescendant on the field, aria-selected on the option), and Enter follows the one they
6
6
  // are on. The page under it does not scroll while it is open. Run on every page by the theme's Base.astro
7
7
  // (blockScripts, packages/editor/site/blockScripts.ts); without a search it does nothing.
8
+ import { atLeast } from '@morphemecms/cms/lib/breakpoints.mjs';
9
+
8
10
  const fold = (text: string) => text.toLocaleLowerCase().normalize('NFKD').replace(/\p{M}/gu, '').trim();
9
11
 
10
12
  export default function search() {
11
- const wide = matchMedia('(width >= 48rem)');
13
+ const wide = matchMedia(atLeast('medium'));
12
14
 
13
15
  document.querySelectorAll<HTMLElement>('[data-search]').forEach((search) => {
14
16
  const bar = search.querySelector<HTMLElement>('.search-bar');
package/worker/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
  // - the visitors' answers in the cookie banner, the consent log (/api/consent, packages/editor/worker/consent.js, in D1), and each hour
21
21
  // those older than two years deleted.
22
22
  // - whether R2 caching can be turned on in the editor: whether Cloudflare has its bucket (/api/r2, packages/editor/worker/r2-status.js).
23
+ // - whether the account has Workers Paid, for the switch "Betalt bygning" in the editor (/api/plan, packages/editor/worker/plan-status.js).
23
24
  // - the editor on its own address when EDITOR_HOST is set (packages/editor/worker/editor-host.js); noindex there too.
24
25
  // - each hour, the pages and posts that are Planlagt published once their time has come (scheduled, packages/editor/worker/publish.js).
25
26
  // - the editor's notifications on the phone, also when the editor is closed (/api/push, packages/editor/worker/push.js): each minute,
@@ -39,6 +40,7 @@ import { access, mayEdit } from './session.js';
39
40
  import { handleGitHub } from './editor-github.js';
40
41
  import { handleConnection } from './store-connection.js';
41
42
  import { handleStore } from './store.js';
43
+ import { handlePlan } from './plan-status.js';
42
44
  import { handleR2 } from './r2-status.js';
43
45
  import { publishScheduled, SCHEDULED_LIST } from './publish.js';
44
46
  import { commitFiles, readFiles } from './github.js';
@@ -150,6 +152,7 @@ export default {
150
152
  if (url.pathname === '/api/not-found') return handleNotFound(request, env);
151
153
  if (url.pathname === '/api/consent') return handleConsent(request, env);
152
154
  if (url.pathname === '/api/r2') return handleR2(request, env);
155
+ if (url.pathname === '/api/plan') return handlePlan(request, env);
153
156
  if (url.pathname.startsWith('/api/github/')) return handleGitHub(request, env, ctx);
154
157
  if (url.pathname === '/api/store/connection') return handleConnection(request, env, ctx);
155
158
  if (url.pathname.startsWith('/api/store/')) return handleStore(request, env, ctx);
@@ -0,0 +1,46 @@
1
+ // Whether the Cloudflare account has Workers Paid, for "Indstillinger" › "Avanceret" › "Bygning" in the editor
2
+ // (packages/editor/src/Advanced.tsx): the switch "Betalt bygning" can only be turned on when Cloudflare says so.
3
+ // Asked of Cloudflare's API with the Worker's own key, the same token as for R2 (the secret CLOUDFLARE_R2_TOKEN, and
4
+ // CLOUDFLARE_ACCOUNT_ID); it also needs Account › Billing › Read (docs/editor.md, Opsætning).
5
+ // GET /api/plan { paid }: "yes" (Workers Paid), "no" (the account does not have it), "no-key" (the Worker has no key),
6
+ // "refused" (Cloudflare said no to the key) or "failed".
7
+ // Only someone who can save the site (write access to the repository on GitHub) can ask.
8
+ import { json } from './not-found.js';
9
+ import { mayEdit } from './session.js';
10
+
11
+ // The states of a subscription that count as having the plan.
12
+ const PAID_STATES = ['paid', 'trial', 'provisioned'];
13
+
14
+ export async function handlePlan(request, env) {
15
+ if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
16
+ if (request.method !== 'GET') return json({ error: 'method' }, 405);
17
+ return json({ paid: await paidAt(env) });
18
+ }
19
+
20
+ /** Whether the account's subscriptions (as Cloudflare lists them) include Workers Paid. Cloudflare writes the plan's id both as
21
+ * WORKERS_PAID and workers_paid; a subscription with no state counts, one that is cancelled or the like does not. */
22
+ export function paidFrom(subscriptions) {
23
+ if (!Array.isArray(subscriptions)) return false;
24
+ return subscriptions.some((subscription) => {
25
+ if (String(subscription?.rate_plan?.id ?? '').toLowerCase() !== 'workers_paid') return false;
26
+ const state = subscription.state;
27
+ return state === undefined || state === null || PAID_STATES.includes(String(state).toLowerCase());
28
+ });
29
+ }
30
+
31
+ async function paidAt(env) {
32
+ const account = env.CLOUDFLARE_ACCOUNT_ID?.trim();
33
+ const token = env.CLOUDFLARE_R2_TOKEN?.trim();
34
+ if (!account || !token) return 'no-key';
35
+ try {
36
+ const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(account)}/subscriptions`, {
37
+ headers: { authorization: `Bearer ${token}` },
38
+ });
39
+ const body = await res.json().catch(() => ({}));
40
+ if (res.ok && body.success) return paidFrom(body.result) ? 'yes' : 'no';
41
+ if (res.status === 401 || res.status === 403) return 'refused';
42
+ return 'failed';
43
+ } catch {
44
+ return 'failed';
45
+ }
46
+ }
package/worker/push.js CHANGED
@@ -26,13 +26,15 @@ import { config } from '@site/config';
26
26
  import { contact } from '@site/worker';
27
27
  import { json } from './not-found.js';
28
28
  import { mayEdit } from './session.js';
29
+ import { giveUpAfter } from '../lib/build-settings.mjs';
29
30
 
30
31
  const VAPID = 'vapid';
31
32
  const DEVICE = 'device:';
32
33
  const WAIT = 'wait:';
33
34
  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;
35
+ // How long the editor waits for a save to be on the site (packages/editor/src/deploy.ts): giveUpAfter(paidBuilds), where
36
+ // paidBuilds is in /editor/version.json. Before a wait is checked that is not known: the longest limit is used.
37
+ const LONGEST = giveUpAfter(false);
36
38
  // A device not seen (the editor not opened on it) for half a year drops out by itself.
37
39
  const KEEP = 180 * 24 * 60 * 60;
38
40
  // How long a push service keeps a notification for a device that is off.
@@ -198,7 +200,7 @@ export async function waitForBuilds(env, now = Date.now()) {
198
200
  const at = Date.parse(wait.at);
199
201
  // A build made before the save cannot have it.
200
202
  const online = !!built.commit && builtAt >= at && (await includes(env, built.commit, wait.commit).catch(() => false));
201
- const late = now - at > GIVE_UP;
203
+ const late = now - at > giveUpAfter(built.paidBuilds === true);
202
204
  if (!online && !late) continue;
203
205
  await env.PUSH.delete(name);
204
206
  const device = await env.PUSH.get(DEVICE + wait.device, 'json');
@@ -239,8 +241,8 @@ export async function handlePush(request, env) {
239
241
  const metadata = { commit: body.commit, device: id, text: text(body.text), failText: text(body.failText), at };
240
242
  if (isRoute(body.to)) metadata.to = body.to;
241
243
  // 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
+ await env.PUSH.put(WAIT + id, '', { metadata, expirationTtl: (2 * LONGEST) / 1000 });
245
+ await env.PUSH.put(WAITING, String(Date.now() + LONGEST + 2 * 60_000));
244
246
  return json({ ok: true });
245
247
  }
246
248
  if (wait && request.method === 'DELETE') {
@@ -2,7 +2,8 @@
2
2
  // editor (packages/editor/src/Advanced.tsx): the switch writes the bucket into the Worker's settings (wrangler.jsonc),
3
3
  // and a deploy with a bucket Cloudflare does not have fails, so it can only be turned on when Cloudflare says the bucket
4
4
  // is there (R2 turned on for the account, and the bucket made). Asked of Cloudflare's API with the Worker's own key, a
5
- // token that may only read R2 (the secret CLOUDFLARE_R2_TOKEN, and CLOUDFLARE_ACCOUNT_ID; docs/editor.md, Opsætning).
5
+ // token that may only read R2 (the secret CLOUDFLARE_R2_TOKEN, and CLOUDFLARE_ACCOUNT_ID; docs/editor.md, Opsætning). The same
6
+ // token also tells whether the account has Workers Paid (packages/editor/worker/plan-status.js), so it needs Billing: Read too.
6
7
  // GET /api/r2?bucket=<name>[&jurisdiction=<eu|fedramp>] { bound, bucket }: whether this Worker has FILES now (the site is built with it), and the
7
8
  // bucket at Cloudflare: "ready" (there), "no-bucket", "no-r2" (R2 not turned on), "no-key" (the Worker has no key),
8
9
  // "refused" (Cloudflare said no to the key) or "failed". A bucket kept within a jurisdiction is only found in it.
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2" /> <line x1="8" x2="16" y1="21" y2="21" /> <line x1="12" x2="12" y1="17" y2="21" /></g></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M334-120v-60h86v-100H140q-24 0-42-18t-18-42v-440q0-24 18-42t42-18h680q24 0 42 18t18 42v440q0 24-18 42t-42 18H540v100h86v60H334ZM140-340h680v-440H140v440Zm0 0v-440 440Z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24h72v16H96a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16H136V200h72a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40ZM48,56H208a8,8,0,0,1,8,8v80H40V64A8,8,0,0,1,48,56ZM208,184H48a8,8,0,0,1-8-8V160H216v16A8,8,0,0,1,208,184Z"/></svg>