@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,44 @@
1
+ // Filer's filter by tag (Gallery in Media.tsx): each file's own tags from its record, the tags the filter offers (only those some file has), and the files with a tag.
2
+ // Pure, so node --test can run it. Uses the StoredRecord type from fileRecords.mjs.
3
+ /**
4
+ * @typedef {import('./fileRecords.mjs').StoredRecord} StoredRecord
5
+ */
6
+
7
+ /**
8
+ * The tags of each file, keyed by file address (src). Records with no tags are left out.
9
+ * @param {StoredRecord[]} records
10
+ * @returns {Map<string, string[]>}
11
+ */
12
+ export function tagIndex(records) {
13
+ const index = new Map();
14
+ for (const stored of records) {
15
+ if (stored.record.categories.length > 0) index.set(stored.record.src, [...stored.record.categories]);
16
+ }
17
+ return index;
18
+ }
19
+
20
+ /**
21
+ * The filter's choices: the tags (in their order) that some file in the index has, plus the chosen tag when it is among the tags.
22
+ * @template {{ value: string; label: string }} O
23
+ * @param {Map<string, string[]>} index
24
+ * @param {O[]} tags
25
+ * @param {string} chosen
26
+ * @returns {O[]}
27
+ */
28
+ export function tagFilterOptions(index, tags, chosen) {
29
+ const used = new Set();
30
+ for (const list of index.values()) for (const tag of list) used.add(tag);
31
+ return tags.filter((option) => used.has(option.value) || option.value === chosen);
32
+ }
33
+
34
+ /**
35
+ * The files that have a tag, in their order. With an empty tag, all files are kept.
36
+ * @param {string[]} srcs
37
+ * @param {Map<string, string[]>} index
38
+ * @param {string} tag
39
+ * @returns {string[]}
40
+ */
41
+ export function withTag(srcs, index, tag) {
42
+ if (tag === '') return srcs;
43
+ return srcs.filter((src) => index.get(src)?.includes(tag) === true);
44
+ }
@@ -0,0 +1,57 @@
1
+ // Tests for src/fileTagFilter.mjs. Run: node --test packages/editor/src/*.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { test } from 'node:test';
4
+ import { tagFilterOptions, tagIndex, withTag } from './fileTagFilter.mjs';
5
+
6
+ /**
7
+ * A stored record for a file address, with its tags and removed tags.
8
+ * @param {string} src
9
+ * @param {string[]} categories
10
+ * @param {string[]} removed
11
+ * @returns {import('./fileRecords.mjs').StoredRecord}
12
+ */
13
+ const rec = (src, categories, removed) => ({
14
+ path: `src/data/files/${src.slice(1, -5)}.json`,
15
+ oid: 'oid',
16
+ record: { src, categories, removed },
17
+ });
18
+
19
+ const TAGS = [
20
+ { value: 'design', label: 'Design' },
21
+ { value: 'seo', label: 'SEO' },
22
+ ];
23
+
24
+ test('tagIndex maps a file address to its tags', () => {
25
+ const index = tagIndex([rec('/a.webp', ['seo'], []), rec('/b.webp', [], ['seo'])]);
26
+ assert.deepEqual(index, new Map([['/a.webp', ['seo']]]));
27
+ });
28
+
29
+ test('tagFilterOptions gives only the tags some file has', () => {
30
+ const index = new Map([['/a.webp', ['seo']]]);
31
+ assert.deepEqual(tagFilterOptions(index, TAGS, ''), [{ value: 'seo', label: 'SEO' }]);
32
+ });
33
+
34
+ test('tagFilterOptions adds the chosen tag when it is among the tags', () => {
35
+ const index = new Map([['/a.webp', ['seo']]]);
36
+ assert.deepEqual(tagFilterOptions(index, TAGS, 'design'), TAGS);
37
+ });
38
+
39
+ test('tagFilterOptions leaves out a chosen tag that is not among the tags', () => {
40
+ const index = new Map([['/a.webp', ['seo']]]);
41
+ assert.deepEqual(tagFilterOptions(index, TAGS, 'gone'), [{ value: 'seo', label: 'SEO' }]);
42
+ });
43
+
44
+ test('withTag keeps only the files with the tag', () => {
45
+ const index = new Map([['/a.webp', ['seo']]]);
46
+ assert.deepEqual(withTag(['/a.webp', '/b.webp'], index, 'seo'), ['/a.webp']);
47
+ });
48
+
49
+ test('withTag keeps every file when the tag is empty', () => {
50
+ const index = new Map([['/a.webp', ['seo']]]);
51
+ assert.deepEqual(withTag(['/a.webp', '/b.webp'], index, ''), ['/a.webp', '/b.webp']);
52
+ });
53
+
54
+ test('withTag gives an empty list when no file has the tag', () => {
55
+ const index = new Map([['/a.webp', ['seo']]]);
56
+ assert.deepEqual(withTag(['/b.webp'], index, 'seo'), []);
57
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A record with no tags for a file.
3
+ * @param {string} src
4
+ * @returns {FileRecord}
5
+ */
6
+ export function emptyRecord(src: string): FileRecord;
7
+ /**
8
+ * The tags of a file: its own tags first, then the page's tags that were not removed from it.
9
+ * @param {FileRecord | undefined} record
10
+ * @param {string[]} pageTags
11
+ * @returns {string[]}
12
+ */
13
+ export function tagsFor(record: FileRecord | undefined, pageTags: string[]): string[];
14
+ /**
15
+ * The record with a tag added; the tag leaves `removed`.
16
+ * @param {FileRecord | undefined} record
17
+ * @param {string} src
18
+ * @param {string} tag
19
+ * @returns {FileRecord}
20
+ */
21
+ export function addTag(record: FileRecord | undefined, src: string, tag: string): FileRecord;
22
+ /**
23
+ * The record with a tag removed; the tag goes to `removed`.
24
+ * @param {FileRecord | undefined} record
25
+ * @param {string} src
26
+ * @param {string} tag
27
+ * @returns {FileRecord}
28
+ */
29
+ export function removeTag(record: FileRecord | undefined, src: string, tag: string): FileRecord;
30
+ /**
31
+ * The record with exactly these tags, in this order. Tags that leave are removed; tags that arrive are added.
32
+ * @param {FileRecord | undefined} record
33
+ * @param {string} src
34
+ * @param {string[]} next
35
+ * @returns {FileRecord}
36
+ */
37
+ export function setTags(record: FileRecord | undefined, src: string, next: string[]): FileRecord;
38
+ /**
39
+ * The record with its tags set to the page's tags (those not removed); `removed` is kept.
40
+ * @param {FileRecord | undefined} record
41
+ * @param {string} src
42
+ * @param {string[]} pageTags
43
+ * @returns {FileRecord}
44
+ */
45
+ export function inherit(record: FileRecord | undefined, src: string, pageTags: string[]): FileRecord;
46
+ /**
47
+ * True when the record holds no tags and no removed tags.
48
+ * @param {FileRecord} record
49
+ * @returns {boolean}
50
+ */
51
+ export function isEmpty(record: FileRecord): boolean;
52
+ export type FileRecord = {
53
+ src: string;
54
+ categories: string[];
55
+ removed: string[];
56
+ };
@@ -0,0 +1,100 @@
1
+ // The tags of one file in Filer and how they change: its own tags and the tags removed from it.
2
+ // A page's tags reach the file only when it is inserted (tagsFor). Pure; used by fileRecords.mjs and MediaTags.tsx.
3
+ /**
4
+ * @typedef {{ src: string; categories: string[]; removed: string[] }} FileRecord
5
+ */
6
+
7
+ /** @param {unknown[]} list */
8
+ const unique = (list) => [...new Set(list.map(String))];
9
+
10
+ /**
11
+ * A record with no tags for a file.
12
+ * @param {string} src
13
+ * @returns {FileRecord}
14
+ */
15
+ export function emptyRecord(src) {
16
+ return { src, categories: [], removed: [] };
17
+ }
18
+
19
+ /**
20
+ * The tags of a file: its own tags first, then the page's tags that were not removed from it.
21
+ * @param {FileRecord | undefined} record
22
+ * @param {string[]} pageTags
23
+ * @returns {string[]}
24
+ */
25
+ export function tagsFor(record, pageTags) {
26
+ const removed = record?.removed ?? [];
27
+ const out = unique(record?.categories ?? []);
28
+ for (const tag of unique(pageTags)) {
29
+ if (!removed.includes(tag) && !out.includes(tag)) out.push(tag);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /**
35
+ * The record with a tag added; the tag leaves `removed`.
36
+ * @param {FileRecord | undefined} record
37
+ * @param {string} src
38
+ * @param {string} tag
39
+ * @returns {FileRecord}
40
+ */
41
+ export function addTag(record, src, tag) {
42
+ const categories = record?.categories ?? [];
43
+ return {
44
+ src,
45
+ categories: categories.includes(tag) ? [...categories] : [...categories, tag],
46
+ removed: (record?.removed ?? []).filter((t) => t !== tag),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * The record with a tag removed; the tag goes to `removed`.
52
+ * @param {FileRecord | undefined} record
53
+ * @param {string} src
54
+ * @param {string} tag
55
+ * @returns {FileRecord}
56
+ */
57
+ export function removeTag(record, src, tag) {
58
+ const removed = record?.removed ?? [];
59
+ return {
60
+ src,
61
+ categories: (record?.categories ?? []).filter((t) => t !== tag),
62
+ removed: removed.includes(tag) ? [...removed] : [...removed, tag],
63
+ };
64
+ }
65
+
66
+ /**
67
+ * The record with exactly these tags, in this order. Tags that leave are removed; tags that arrive are added.
68
+ * @param {FileRecord | undefined} record
69
+ * @param {string} src
70
+ * @param {string[]} next
71
+ * @returns {FileRecord}
72
+ */
73
+ export function setTags(record, src, next) {
74
+ let current = record ?? emptyRecord(src);
75
+ for (const tag of current.categories) {
76
+ if (!next.includes(tag)) current = removeTag(current, src, tag);
77
+ }
78
+ for (const tag of next) current = addTag(current, src, tag);
79
+ return { src, categories: unique(next), removed: [...current.removed] };
80
+ }
81
+
82
+ /**
83
+ * The record with its tags set to the page's tags (those not removed); `removed` is kept.
84
+ * @param {FileRecord | undefined} record
85
+ * @param {string} src
86
+ * @param {string[]} pageTags
87
+ * @returns {FileRecord}
88
+ */
89
+ export function inherit(record, src, pageTags) {
90
+ return { src, categories: tagsFor(record, pageTags), removed: [...(record?.removed ?? [])] };
91
+ }
92
+
93
+ /**
94
+ * True when the record holds no tags and no removed tags.
95
+ * @param {FileRecord} record
96
+ * @returns {boolean}
97
+ */
98
+ export function isEmpty(record) {
99
+ return record.categories.length === 0 && record.removed.length === 0;
100
+ }
@@ -0,0 +1,127 @@
1
+ // Tests for src/fileTags.mjs. Run: node --test packages/editor/src/*.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { test } from 'node:test';
4
+ import { addTag, emptyRecord, inherit, isEmpty, removeTag, setTags, tagsFor } from './fileTags.mjs';
5
+
6
+ const S = '/images/uploads/2026/10/andromeda.webp';
7
+
8
+ /** Fails when a record has a slug in both lists. @param {{ categories: string[]; removed: string[] }} r */
9
+ const assertInvariant = (r) => {
10
+ for (const tag of r.categories) assert.ok(!r.removed.includes(tag), `${tag} is in both lists`);
11
+ };
12
+
13
+ /** A record that cannot be changed, lists included. @param {{ src: string; categories: string[]; removed: string[] }} r */
14
+ const frozen = (r) => Object.freeze({ src: r.src, categories: Object.freeze([...r.categories]), removed: Object.freeze([...r.removed]) });
15
+
16
+ test('emptyRecord has no tags', () => {
17
+ assert.deepEqual(emptyRecord(S), { src: S, categories: [], removed: [] });
18
+ });
19
+
20
+ test('tagsFor gives the page tags for a file without a record', () => {
21
+ assert.deepEqual(tagsFor(undefined, ['a', 'b']), ['a', 'b']);
22
+ });
23
+
24
+ test('tagsFor gives the record tags first, then the page tags', () => {
25
+ assert.deepEqual(tagsFor({ src: S, categories: ['x'], removed: [] }, ['a']), ['x', 'a']);
26
+ });
27
+
28
+ test('tagsFor does not repeat a tag the record already has', () => {
29
+ assert.deepEqual(tagsFor({ src: S, categories: ['a'], removed: [] }, ['a', 'b']), ['a', 'b']);
30
+ });
31
+
32
+ test('tagsFor leaves out a page tag the owner removed', () => {
33
+ assert.deepEqual(tagsFor({ src: S, categories: [], removed: ['a'] }, ['a', 'b']), ['b']);
34
+ });
35
+
36
+ test('tagsFor keeps the record tags even when the page has them removed', () => {
37
+ assert.deepEqual(tagsFor({ src: S, categories: ['x'], removed: ['a'] }, []), ['x']);
38
+ });
39
+
40
+ test('tagsFor gives each page tag once', () => {
41
+ assert.deepEqual(tagsFor(undefined, ['a', 'a']), ['a']);
42
+ });
43
+
44
+ test('addTag creates a record for a file without one', () => {
45
+ assert.deepEqual(addTag(undefined, S, 'seo'), { src: S, categories: ['seo'], removed: [] });
46
+ });
47
+
48
+ test('addTag takes a tag out of removed', () => {
49
+ assert.deepEqual(addTag({ src: S, categories: ['a'], removed: ['seo'] }, S, 'seo'), {
50
+ src: S,
51
+ categories: ['a', 'seo'],
52
+ removed: [],
53
+ });
54
+ });
55
+
56
+ test('addTag does nothing to a tag the file already has', () => {
57
+ assert.deepEqual(addTag({ src: S, categories: ['a'], removed: [] }, S, 'a'), { src: S, categories: ['a'], removed: [] });
58
+ });
59
+
60
+ test('removeTag moves a tag to removed', () => {
61
+ assert.deepEqual(removeTag({ src: S, categories: ['a', 'b'], removed: [] }, S, 'a'), {
62
+ src: S,
63
+ categories: ['b'],
64
+ removed: ['a'],
65
+ });
66
+ });
67
+
68
+ test('removeTag creates a record for a file without one', () => {
69
+ assert.deepEqual(removeTag(undefined, S, 'a'), { src: S, categories: [], removed: ['a'] });
70
+ });
71
+
72
+ test('removeTag does not repeat a tag already removed', () => {
73
+ assert.deepEqual(removeTag({ src: S, categories: [], removed: ['a'] }, S, 'a'), { src: S, categories: [], removed: ['a'] });
74
+ });
75
+
76
+ test('setTags moves the tags that left to removed and keeps the order of the new list', () => {
77
+ assert.deepEqual(setTags({ src: S, categories: ['a', 'b'], removed: ['c'] }, S, ['b', 'c']), {
78
+ src: S,
79
+ categories: ['b', 'c'],
80
+ removed: ['a'],
81
+ });
82
+ });
83
+
84
+ test('setTags with no tags on a file without a record gives an empty record', () => {
85
+ assert.deepEqual(setTags(undefined, S, []), { src: S, categories: [], removed: [] });
86
+ });
87
+
88
+ test('inherit adds the page tags that were not removed and keeps removed', () => {
89
+ assert.deepEqual(inherit({ src: S, categories: ['x'], removed: ['a'] }, S, ['a', 'b']), {
90
+ src: S,
91
+ categories: ['x', 'b'],
92
+ removed: ['a'],
93
+ });
94
+ });
95
+
96
+ test('inherit gives the page tags to a file without a record', () => {
97
+ assert.deepEqual(inherit(undefined, S, ['a']), { src: S, categories: ['a'], removed: [] });
98
+ });
99
+
100
+ test('isEmpty is true for a record with no tags and nothing removed', () => {
101
+ assert.equal(isEmpty({ src: S, categories: [], removed: [] }), true);
102
+ });
103
+
104
+ test('isEmpty is false for a record with only a removed tag', () => {
105
+ assert.equal(isEmpty({ src: S, categories: [], removed: ['a'] }), false);
106
+ });
107
+
108
+ test('every record result keeps categories and removed apart', () => {
109
+ assertInvariant(addTag({ src: S, categories: ['a'], removed: ['seo'] }, S, 'seo'));
110
+ assertInvariant(addTag(undefined, S, 'seo'));
111
+ assertInvariant(removeTag({ src: S, categories: ['a', 'b'], removed: [] }, S, 'a'));
112
+ assertInvariant(removeTag(undefined, S, 'a'));
113
+ assertInvariant(setTags({ src: S, categories: ['a', 'b'], removed: ['c'] }, S, ['b', 'c']));
114
+ assertInvariant(setTags(undefined, S, []));
115
+ assertInvariant(inherit({ src: S, categories: ['x'], removed: ['a'] }, S, ['a', 'b']));
116
+ assertInvariant(inherit(undefined, S, ['a']));
117
+ });
118
+
119
+ test('no function changes a frozen input record', () => {
120
+ const record = frozen({ src: S, categories: ['a', 'b'], removed: ['c'] });
121
+ assert.doesNotThrow(() => addTag(record, S, 'd'));
122
+ assert.doesNotThrow(() => removeTag(record, S, 'a'));
123
+ assert.doesNotThrow(() => setTags(record, S, ['b', 'c']));
124
+ assert.doesNotThrow(() => inherit(record, S, ['c', 'e']));
125
+ assert.doesNotThrow(() => tagsFor(record, ['c', 'e']));
126
+ assert.deepEqual(record, { src: S, categories: ['a', 'b'], removed: ['c'] });
127
+ });
@@ -0,0 +1,6 @@
1
+ export function topHeadings(doc: import("@milkdown/kit/prose/model").Node): {
2
+ node: import("@milkdown/kit/prose/model").Node;
3
+ pos: number;
4
+ }[];
5
+ /** @type {import('@milkdown/kit/utils').$Prose} */
6
+ export const firstHeading: import("@milkdown/kit/utils").$Prose;
@@ -0,0 +1,37 @@
1
+ // On a page the first heading of the text is its H1 by itself and no later heading is: a ProseMirror plugin for Milkdown
2
+ // that, after a change to the text, sets the levels by headingLevels (lib/heading-levels.mjs). Exposes firstHeading, which
3
+ // MarkdownEditor.tsx uses when it gets `h1` (pages), as the round-trip check does; topHeadings reads the headings. Only top-level headings count.
4
+ import { $prose } from '@milkdown/kit/utils';
5
+ import { Plugin } from '@milkdown/kit/prose/state';
6
+ import { headingLevels } from '../lib/heading-levels.mjs';
7
+
8
+ /** The top-level headings of a document, with their positions.
9
+ * @param {import('@milkdown/kit/prose/model').Node} doc
10
+ * @returns {{ node: import('@milkdown/kit/prose/model').Node, pos: number }[]} */
11
+ export const topHeadings = (doc) => {
12
+ const found = [];
13
+ doc.forEach((node, pos) => void (node.type.name === 'heading' && found.push({ node, pos })));
14
+ return found;
15
+ };
16
+
17
+ /** @type {import('@milkdown/kit/utils').$Prose} */
18
+ export const firstHeading = $prose(
19
+ () =>
20
+ new Plugin({
21
+ appendTransaction: (transactions, _before, state) => {
22
+ // Only after a change the user made: not when the page is only opened, where Milkdown's own steps (the heading ids,
23
+ // the free lines) change the document too, but are kept out of undo (addToHistory false).
24
+ if (!transactions.some((tr) => tr.docChanged && tr.getMeta('addToHistory') !== false)) return null;
25
+ const found = topHeadings(state.doc);
26
+ const levels = headingLevels(found.map(({ node }) => node.attrs.level));
27
+ let tr = null;
28
+ found.forEach(({ node, pos }, i) => {
29
+ if (node.attrs.level === levels[i]) return;
30
+ // The heading keeps its size, so the positions after it stay the same.
31
+ tr ??= state.tr;
32
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, level: levels[i] });
33
+ });
34
+ return tr;
35
+ },
36
+ }),
37
+ );
@@ -1,28 +1,43 @@
1
1
  import type { Messages } from '../en-US';
2
2
 
3
3
  export const advanced: Messages['advanced'] = {
4
- r2Caching: 'R2-caching',
5
- r2CachingHint: 'Workeren giver editoren hjemmesidens filer fra Cloudflare R2 og spørger kun GitHub om det, der er ændret.',
6
- checking: 'Spørger Cloudflare, om R2 er klar.',
4
+ r2Caching: 'Hurtigere editor',
5
+ r2CachingHint: 'Editoren henter hjemmesidens filer hurtigere og spørger kun GitHub om det, der er ændret.',
6
+ checking: 'Tjekker, om Cloudflare er klar…',
7
7
  bucket: {
8
- ready: (bucket, jurisdiction) => `Bucket'en ${bucket} er klar hos Cloudflare${jurisdiction ? ` i ${jurisdiction}` : ''}.`,
8
+ ready: (bucket, jurisdiction) => `Lageret ${bucket} er klar hos Cloudflare${jurisdiction ? ` i ${jurisdiction}` : ''}.`,
9
9
  'no-bucket': (bucket, jurisdiction) =>
10
- `Kan slås til, når bucket'en ${bucket} er lavet under R2 hos Cloudflare${jurisdiction ? ` med jurisdiktionen ${jurisdiction}` : ''}.`,
10
+ `Kan slås til, når lageret ${bucket} er lavet under R2 hos Cloudflare${jurisdiction ? ` og ligger i ${jurisdiction}` : ''}.`,
11
11
  'no-r2': () => 'Kan slås til, når R2 er slået til hos Cloudflare.',
12
- 'no-key': () => 'Workeren har ingen nøgle til Cloudflare, så den kan ikke se, om R2 er slået til.',
13
- refused: () => 'Cloudflare godtog ikke Workerens nøgle til R2.',
12
+ 'no-key': () => 'Editoren har ingen nøgle til Cloudflare, så den kan ikke se, om R2 er slået til.',
13
+ refused: () => 'Cloudflare godtog ikke editorens nøgle til R2.',
14
14
  failed: () => 'Cloudflare svarede ikke på, om R2 er klar. Prøv igen senere.',
15
15
  },
16
+ builds: 'Opdatering af hjemmesiden',
17
+ fastContentBuilds: 'Hurtigere opdateringer af indhold',
18
+ fastContentBuildsHint: 'Hjemmesiden bliver opdateret hurtigere, når du kun har ændret indhold.',
19
+ paidBuilds: 'Workers Paid-plan',
20
+ paidBuildsHint: 'Slå til, når Cloudflare-kontoen har Workers Paid.',
21
+ plan: {
22
+ no: 'Kan slås til, når Cloudflare-kontoen har Workers Paid.',
23
+ 'no-key': 'Editoren har ingen nøgle til Cloudflare, så den kan ikke se, om kontoen har Workers Paid.',
24
+ refused: 'Cloudflare lod ikke editorens nøgle læse kontoens plan. Nøglen skal have Billing: Read.',
25
+ failed: 'Cloudflare svarede ikke på, om kontoen har Workers Paid. Prøv igen senere.',
26
+ },
27
+ billing: 'Åbn betaling hos Cloudflare',
28
+ buildSettingsNotLoaded: (error) => `Indstillingerne for opdatering af hjemmesiden kunne ikke hentes: ${error}`,
16
29
  place: 'Placering',
17
- placeHint: 'Kan kun skiftes, når R2-caching er slået fra.',
30
+ placeHint: 'Kan kun skiftes, når Hurtigere editor er slået fra.',
18
31
  placeAuto: 'Automatisk',
19
32
  onWhenBuilt: 'Slås til, når hjemmesiden er opdateret.',
20
33
  offWhenBuilt: 'Slås fra, når hjemmesiden er opdateret.',
21
- loading: 'Henter Workerens indstillinger',
22
- notLoaded: (error) => `Workerens indstillinger kunne ikke hentes: ${error}`,
23
- noSettings: 'Hjemmesiden har ingen indstillinger til Workeren (wrangler.jsonc).',
24
- noLine: 'Workerens indstillinger (wrangler.jsonc) har ingen linje til R2-caching.',
34
+ loading: 'Henter indstillingerne…',
35
+ notLoaded: (error) => `Indstillingerne kunne ikke hentes: ${error}`,
36
+ noSettings: 'Hjemmesiden har ingen indstillinger til editoren',
37
+ noLine: 'Hjemmesidens indstillinger har ingen plads til Hurtigere editor',
25
38
  r2CachingCommit: (on) => (on ? 'Editor: slå R2-caching til' : 'Editor: slå R2-caching fra'),
39
+ fastContentBuildsCommit: (on) => (on ? 'Editor: slå hurtig bygning af indhold til' : 'Editor: slå hurtig bygning af indhold fra'),
40
+ paidBuildsCommit: (on) => (on ? 'Editor: slå betalt bygning til' : 'Editor: slå betalt bygning fra'),
26
41
  placeCommit: (jurisdiction) =>
27
42
  jurisdiction ? `Editor: hold R2-cachingens bucket i ${jurisdiction}` : 'Editor: lad Cloudflare placere R2-cachingens bucket',
28
43
  };
@@ -2,10 +2,10 @@ import type { Messages } from '../en-US';
2
2
 
3
3
  export const app: Messages['app'] = {
4
4
  logIn: 'Log ind med GitHub',
5
- loading: 'Henter hjemmesidens indhold',
5
+ loading: 'Henter hjemmesidens indhold…',
6
6
  cannotSave: (login) => `${login} har ikke adgang til at gemme. Log ind med en anden GitHub-konto under Konto.`,
7
7
  notFound: 'Findes ikke',
8
- nothingAt: (route) => `Der er ikke noget på adressen ${route}.`,
8
+ nothingAt: (route) => `Der er ikke noget på adressen ${route}`,
9
9
  siteUpdated: (changes) => `Hjemmesiden er opdateret: ${changes}.`,
10
10
  siteNotUpdated: (changes) => `Hjemmesiden blev ikke opdateret (${changes}). Gendan den forrige version under Tidligere versioner.`,
11
11
  changes: {
@@ -35,8 +35,10 @@ export const app: Messages['app'] = {
35
35
  redirect: (from) => `videresendelse fra ${from}`,
36
36
  logo: 'logo',
37
37
  icon: 'ikon',
38
- r2Caching: 'R2-caching',
38
+ r2Caching: 'hurtigere editor',
39
39
  place: 'placering',
40
+ fastContentBuilds: 'hurtigere opdateringer af indhold',
41
+ paidBuilds: 'Workers Paid-plan',
40
42
  },
41
43
  addressTaken: (address) => `Der findes allerede en side eller et blogindlæg med adressen ${address}.`,
42
44
  addressRedirects: (address, name) => `Adressen ${address} sender i dag besøgende videre til ${name}.`,
@@ -47,20 +49,20 @@ export const app: Messages['app'] = {
47
49
  lastSaved: (when) => `Sidst ændret ${when}`,
48
50
  notSavedYet: 'Ikke gemt endnu',
49
51
  unsavedTitle: 'Gem ændringerne?',
50
- unsaved: 'Du har ændringer, der ikke er gemt.',
52
+ unsaved: 'Du har ændringer, der ikke er gemt',
51
53
  discard: 'Kassér',
52
54
  unsavedSheet: 'Du har ændringer, der ikke er gemt. Vil du beholde dem?',
53
55
  viewOnSite: 'Se på hjemmesiden',
54
56
  loggedOut: 'Du er blevet logget ud. Log ind igen.',
55
- githubAnswered: (status) => `GitHub svarede ${status}.`,
57
+ githubAnswered: (status) => `GitHub svarede med fejl ${status}. Prøv igen.`,
56
58
  pictureNotLoaded: 'Billedet kunne ikke hentes fra GitHub. Tjek forbindelsen og prøv igen.',
57
59
  pictureNotWhole: 'Billedet kunne ikke hentes helt. Prøv igen.',
58
60
  changedElsewhere: 'Filen er ændret et andet sted. Kopiér din tekst, og genindlæs siden.',
59
61
  fileExists: 'Der findes allerede en fil med det navn. Prøv igen, eller vælg en anden titel.',
60
62
  pictureNotOpened: 'Billedet kunne ikke åbnes. Brug et JPEG-, PNG- eller WebP-billede.',
61
- pictureNotReady: 'Billedet kunne ikke klargøres.',
63
+ pictureNotReady: 'Billedet kunne ikke klargøres',
62
64
  preparingPicture: 'Klargør billedet…',
63
- invalidJson: 'Filen er ikke gyldig JSON.',
65
+ invalidJson: 'Filen er ikke gyldig JSON',
64
66
  blocksOff: 'Slået fra',
65
67
  block: 'Blok',
66
68
  appsInstalled: 'Installeret',
@@ -12,7 +12,7 @@ export const apps: Messages['apps'] = {
12
12
  needsLoads: 'Henter fra andre adresser',
13
13
  needsSends: 'Sender formularer til andre adresser',
14
14
  needsServer: 'Har sin egen del af hjemmesidens server',
15
- needsBuild: 'Laver sine egne filer, når hjemmesiden bygges',
15
+ needsBuild: 'Laver sine egne filer, når hjemmesiden opdateres',
16
16
  changes: { 'page-colors': 'Ændrer sidernes farver' },
17
17
  unknownHook: (places) => `Filen er ikke et sted, en app kan ændre hjemmesiden: ${places}.`,
18
18
  needsNothing: 'Ingen',
@@ -25,7 +25,7 @@ export const apps: Messages['apps'] = {
25
25
  developer: 'Udvikler',
26
26
  developerApps: 'Apps',
27
27
  appCount: (n) => (n === 1 ? '1 app' : `${n} apps`),
28
- noDeveloper: 'Der er ingen apps fra den udvikler.',
28
+ noDeveloper: 'Der er ingen apps fra den udvikler',
29
29
  unknownScreenshot: (shows) =>
30
30
  `Billedet "${shows}" viser intet, appen har: blok:<type>, slags:<liste>:<variant>, liste:<id>, indstillinger eller side.`,
31
31
  noScreenshot: (file) => `${file} findes ikke. Tag billederne med npx morpheme app-screenshots.`,
@@ -35,24 +35,24 @@ export const apps: Messages['apps'] = {
35
35
  installing: 'Installerer…',
36
36
  uninstalling: 'Afinstallerer…',
37
37
  updating: 'Opdaterer…',
38
- isInstalled: (app) => `${app} er installeret.`,
39
- isUninstalled: (app) => `${app} er afinstalleret.`,
40
- isInstalledSoon: (app) => `${app} er installeret og kan bruges, når hjemmesiden er bygget.`,
41
- isUpdatedSoon: (app, version) => `${app} er opdateret til ${version} og virker sådan, når hjemmesiden er bygget.`,
38
+ isInstalled: (app) => `${app} er installeret`,
39
+ isUninstalled: (app) => `${app} er afinstalleret`,
40
+ isInstalledSoon: (app) => `${app} er installeret og kan bruges om et par minutter`,
41
+ isUpdatedSoon: (app, version) => `${app} er opdateret til ${version}. Hjemmesiden bruger den om et par minutter.`,
42
42
  askUninstall: (app) => `Afinstaller ${app}? Det, appen tilføjer, forsvinder fra editoren.`,
43
43
  activeOf: (block) => `Aktiveret: ${block}`,
44
44
  uninstallHint: 'Kan afinstalleres, fordi intet på hjemmesiden bruger appen.',
45
45
  used: 'Bruges på',
46
- notUsed: 'Bruges ingen steder.',
46
+ notUsed: 'Bruges ingen steder',
47
47
  stuck: 'Appen bruges og kan ikke afinstalleres. Fjern den fra stederne ovenfor først.',
48
48
  blocks: 'Blokke',
49
49
  lists: 'Lister',
50
50
  settings: 'Indstillinger',
51
51
  find: 'Appstore',
52
52
  searchStore: 'Søg efter apps',
53
- loadingStore: 'Henter apps',
54
- storeFailed: 'Kunne ikke hente Appstore.',
55
- noStore: 'Sitet har ingen Appstore.',
53
+ loadingStore: 'Henter apps…',
54
+ storeFailed: 'Kunne ikke hente Appstore',
55
+ noStore: 'Hjemmesiden har ingen Appstore',
56
56
  connectTitle: 'Hjemmesiden er ikke forbundet til Morpheme',
57
57
  connectText: 'Forbind den for at installere apps og temaer fra Appstore.',
58
58
  connect: 'Forbind til Morpheme',
@@ -60,8 +60,8 @@ export const apps: Messages['apps'] = {
60
60
  disconnect: 'Afbryd forbindelsen til Morpheme',
61
61
  disconnectAsk: 'Afbryd hjemmesidens forbindelse til Morpheme? Installerede apps og temaer bliver.',
62
62
  connectBusy: 'Mange hjemmesider forbinder lige nu. Prøv igen om en time.',
63
- storeBroken: (app) => `${app} kunne ikke hentes rigtigt fra Appstore.`,
64
- notInStore: 'Appen findes ikke i Appstore.',
63
+ storeBroken: (app) => `${app} kunne ikke hentes rigtigt fra Appstore`,
64
+ notInStore: 'Appen findes ikke i Appstore',
65
65
  categoryFilter: 'Kategori',
66
66
  allCategories: 'Alle kategorier',
67
67
  stateFilter: 'Status',
@@ -75,18 +75,18 @@ export const apps: Messages['apps'] = {
75
75
  design: 'Udseende',
76
76
  },
77
77
  version: (version) => `Version ${version}`,
78
- pending: 'Installeres …',
79
- pendingHint: 'Appen kan bruges, når hjemmesiden er bygget.',
78
+ pending: 'Installeres…',
79
+ pendingHint: 'Appen kan bruges om et par minutter.',
80
80
  updateFound: 'Opdatering findes',
81
81
  update: (version) => `Opdater til ${version}`,
82
82
  askUpdate: (app, version) => `Opdater ${app} til ${version}?`,
83
83
  overwrites: 'Det, der er rettet i filerne her, bliver overskrevet.',
84
84
  updateNow: 'Opdater',
85
- autoUpdated: (app, version) => `${app} er opdateret til ${version}.`,
85
+ autoUpdated: (app, version) => `${app} er opdateret til ${version}`,
86
86
  whatsNew: 'Se, hvad der er nyt',
87
87
  breakingFound: (app, version) => `${app} ${version} findes. Den kan ødelægge noget, så den opdateres ikke af sig selv.`,
88
88
  breaking: 'Versionen har ændringer, der kan ødelægge noget.',
89
- noneHere: 'Ingen installerede apps har indstillinger her.',
89
+ noneHere: 'Ingen installerede apps har indstillinger her',
90
90
  settingsOf: (app) => `indstillinger for ${app}`,
91
91
  missing: (id, folder) => `Appen "${id}" er installeret, men ${folder}/ findes ikke.`,
92
92
  commit: {