@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,19 @@
1
+ // The Material Symbols (Outlined) files as the module `virtual:material-symbols-ssr`, answered by themePlugin (theme.mjs):
2
+ // all of them as text on the server render (so a backup icon, icon.tsx, is in the built page's HTML at once), nothing in the browser
3
+ // (where src/materialIcons.ts is the one lazy chunk). Exposes MATERIAL_SSR and materialSsrModule(). Reads @material-symbols/svg-400.
4
+ import { readdirSync, readFileSync } from 'node:fs';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ /** The virtual module's id as imported (packages/editor/src/icon.tsx). */
8
+ export const MATERIAL_SSR = 'virtual:material-symbols-ssr';
9
+
10
+ /** The module's source: `{ '/node_modules/@material-symbols/svg-400/outlined/<name>.svg': '<svg …>' }`, the keys as
11
+ * import.meta.glob names them (src/materialIcons.ts). With `ssr` false: an empty object. */
12
+ export function materialSsrModule(ssr) {
13
+ if (!ssr) return 'export default {};';
14
+ const dir = new URL('node_modules/@material-symbols/svg-400/outlined/', pathToFileURL(`${process.cwd()}/`));
15
+ const files = {};
16
+ for (const name of readdirSync(dir))
17
+ if (name.endsWith('.svg')) files[`/node_modules/@material-symbols/svg-400/outlined/${name}`] = readFileSync(new URL(name, dir), 'utf8');
18
+ return `export default ${JSON.stringify(files)};`;
19
+ }
package/site/sections.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // The space above a section of the blog page and the 404 page (packages/editor/site/standardPages.ts): less above the first, right
2
2
  // under the page's header.
3
- export const space = (i: number) => (i === 0 ? 'mt-10 md:mt-14' : 'mt-16 md:mt-24');
3
+ export const space = (i: number) => (i === 0 ? 'mt-10 medium:mt-14' : 'mt-16 medium:mt-24');
package/site/theme.d.mts CHANGED
@@ -66,7 +66,7 @@ export function themePlugin(): {
66
66
  optimizeDeps?: undefined;
67
67
  };
68
68
  resolveId(source: any, importer: any): string | null;
69
- load(id: any): string | null;
69
+ load(id: any, options: any): string | null;
70
70
  };
71
71
  /** What a block is missing to be drawn from its view (packages/editor/src/standard/view.tsx): a standard block's
72
72
  * contract its view.ts and load.ts, an app's block (src/apps/<app>/blocks/<type>/) its viewOf.ts, load.ts and View.tsx; and the
package/site/theme.mjs CHANGED
@@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
11
11
  import { svgParts } from '@morphemecms/cms/lib/svg.mjs';
12
12
  import { declarationProblems, ownerId } from '@morphemecms/cms/lib/licenses.mjs';
13
13
  import { installedPacks, isIconName, packFolder, packProblems, phosphorFile, svgProblems, usedIcons } from './icons.mjs';
14
+ import { MATERIAL_SSR, materialSsrModule } from './materialSymbols.mjs';
14
15
  import { ICON_PACK_FILE, ICON_PACK_LICENSE, SYSTEM_PACKS, THEME_PACKS_FOLDER, packInUse, packTheme, themePackId } from './iconPacks.mjs';
15
16
  import { MOTION_SCHEMES } from '@morphemecms/cms/lib/motion-schemes.mjs';
16
17
  import { storeImportProblems } from './storeTheme.mjs';
@@ -226,6 +227,7 @@ const PHOSPHOR_REACT = '@phosphor-icons/react';
226
227
  const PHOSPHOR_FILE = /@phosphor-icons\/core\/assets\/(?:regular|fill)\/([a-z0-9-]+)\.svg\?raw$/;
227
228
  const REACT_ICONS = '\0site-theme-icons';
228
229
  const FILE_ICON = '\0site-theme-icon:';
230
+ const MATERIAL_SSR_ID = `\0${MATERIAL_SSR}`;
229
231
 
230
232
  /** The React component's name of a Phosphor icon by its file's name (`caret-right` → CaretRight), when Phosphor has it. */
231
233
  const reactName = (name) => {
@@ -455,6 +457,8 @@ export const aliases = () => ({
455
457
  * pack and no icons of its own nothing is changed. The pack is read when the site is built, so a new choice
456
458
  * (src/data/icons.json) or theme is in the site and the editor after the next build. */
457
459
  export function themePlugin() {
460
+ // With forward slashes, as Vite writes ids: on Windows a mixed id (C:\…\theme\components/MobileNav.astro) hides a
461
+ // component's script from Astro, which looks it up with forward slashes.
458
462
  const themes = [activeTheme(), STANDARD_THEME].map((t) => fileURLToPath(new URL(`${themeFolder(t)}/`, root)).replaceAll('\\', '/'));
459
463
  const icons = themeIcons();
460
464
  const own = Object.keys(icons).length > 0;
@@ -467,6 +471,7 @@ export function themePlugin() {
467
471
  // The theme's icons import Phosphor's own.
468
472
  if (source === PHOSPHOR_REACT) return own && importer !== REACT_ICONS ? REACT_ICONS : null;
469
473
  if (source === ALL_ICONS) return '\0' + ALL_ICONS;
474
+ if (source === MATERIAL_SSR) return MATERIAL_SSR_ID;
470
475
  const icon = source.match(PHOSPHOR_FILE)?.[1];
471
476
  if (icon) return svgParts(icons[icon]) ? FILE_ICON + icon : null;
472
477
  if (source.startsWith('@site/')) return siteFile(source.slice('@site/'.length));
@@ -479,7 +484,8 @@ export function themePlugin() {
479
484
  if (statSync(dir + file + ext, { throwIfNoEntry: false })?.isFile()) return dir + file + ext;
480
485
  return null;
481
486
  },
482
- load(id) {
487
+ load(id, options) {
488
+ if (id === MATERIAL_SSR_ID) return materialSsrModule(options?.ssr);
483
489
  if (id === REACT_ICONS) return reactIcons(icons);
484
490
  if (id === '\0' + ALL_ICONS)
485
491
  return allIconsModule(fileURLToPath(new URL('node_modules/@phosphor-icons/core/assets/regular/', root)), icons);
@@ -187,7 +187,7 @@ export default function ActionParts() {
187
187
  <SiteFabMenu key={size} size={size} icon="chats" label={say.contact} closeLabel={say.close} items={fabItems} />
188
188
  ))}
189
189
  </div>
190
- <div className="grid gap-6 sm:grid-cols-2">
190
+ <div className="grid gap-6 medium:grid-cols-2">
191
191
  {fabMenuKinds.map((kind) => (
192
192
  <div key={kind} className="flex justify-end pt-52">
193
193
  <SiteFabMenu kind={kind} icon="chats" label={say.contact} closeLabel={say.close} items={fabItems} open />
package/src/Advanced.tsx CHANGED
@@ -3,10 +3,13 @@
3
3
  // saved at once in its own commit. Off, the Worker asks GitHub for everything. It can only be turned on when Cloudflare
4
4
  // has the bucket (the Worker asks, /api/r2), else a deploy would fail; under the switch, why not, and, until the site is
5
5
  // built with the change, that it comes then. Above it, where the bucket is kept ("Placering": placed by Cloudflare, or
6
- // within the EU), changed only while it is off, also in its own commit.
6
+ // within the EU, a MenuRow with the choice in a dropdown at the right), changed only while it is off, also in its own commit. Under it, the two build switches ("Builds":
7
+ // fast content builds, paid build plan) in src/data/build.json (buildSettings.ts), each saved at once in its own commit. The paid
8
+ // build plan can only be turned on when Cloudflare says the account has Workers Paid (the Worker asks, /api/plan); under it, why not.
7
9
  import { useEffect, useState } from 'react';
10
+ import { ArrowSquareOut } from '@phosphor-icons/react';
8
11
  import Screen from './Screen';
9
- import { EmptyNote, FormField, GroupTitle, ProblemNote, SegmentedButton, SupportingText, SwitchRow } from './ui';
12
+ import { EmptyNote, GroupTitle, LinkButton, MenuRow, ProblemNote, SupportingText, SwitchRow } from './ui';
10
13
  import { ConflictError, type RepoFile } from './github';
11
14
  import type { Env } from './App';
12
15
  import { savedText } from './deploy';
@@ -17,12 +20,16 @@ import {
17
20
  jurisdictionName,
18
21
  jurisdictionOf,
19
22
  r2CachingOn,
23
+ planStatus,
20
24
  r2Status,
21
25
  withJurisdiction,
22
26
  withR2Caching,
23
27
  type BucketState,
28
+ type PaidState,
24
29
  } from './r2Caching';
25
30
  import { config } from '@site/editor';
31
+ import { buildSettingsOf, withBuildSetting, type BuildSetting } from './buildSettings';
32
+ import { BUILD_SETTINGS_PATH } from '../lib/build-settings.mjs';
26
33
  import { siteT, t } from './i18n';
27
34
 
28
35
  export const ADVANCED_ROUTE = 'avanceret';
@@ -30,12 +37,21 @@ export const ADVANCED_ROUTE = 'avanceret';
30
37
  // "Placering" when Cloudflare places the bucket (no jurisdiction).
31
38
  const AUTO = 'auto';
32
39
 
40
+ // Cloudflare's dashboard sends on to the account's Billing page.
41
+ const BILLING_URL = 'https://dash.cloudflare.com/?to=/:account/billing';
42
+
33
43
  export default function Advanced({ env }: { env: Env }) {
34
44
  const [file, setFile] = useState<RepoFile | null>(null);
35
45
  const [status, setStatus] = useState<{ bound: boolean; bucket: BucketState } | null>(null);
46
+ // The build settings file: undefined while it loads, null when the site has none yet (the defaults apply).
47
+ const [build, setBuild] = useState<RepoFile | null | undefined>(undefined);
48
+ // What Cloudflare says of the account's plan: null while it is asked.
49
+ const [paid, setPaid] = useState<PaidState | null>(null);
50
+ const [buildProblem, setBuildProblem] = useState('');
36
51
  const [problem, setProblem] = useState('');
37
52
  const [saving, setSaving] = useState(false);
38
53
  useEffect(() => {
54
+ planStatus().then(setPaid);
39
55
  env
40
56
  .load([config.wrangler])
41
57
  .then(([found]) => {
@@ -44,6 +60,10 @@ export default function Advanced({ env }: { env: Env }) {
44
60
  ask(found.text);
45
61
  })
46
62
  .catch((e) => setProblem(t.advanced.notLoaded((e as Error).message)));
63
+ env
64
+ .load([BUILD_SETTINGS_PATH])
65
+ .then(([found]) => setBuild(found ?? null))
66
+ .catch((e) => setBuildProblem(t.advanced.buildSettingsNotLoaded((e as Error).message)));
47
67
  }, []);
48
68
 
49
69
  // What Cloudflare says of the bucket the settings name, where they say it is kept.
@@ -74,26 +94,64 @@ export default function Advanced({ env }: { env: Env }) {
74
94
  ...(jurisdiction && !(JURISDICTIONS as readonly string[]).includes(jurisdiction) ? [jurisdiction] : []),
75
95
  ];
76
96
 
77
- async function save(text: string, message: string, change: string): Promise<boolean> {
78
- if (!file) return false;
79
- const path = file.path;
97
+ // Saves one file as its own commit (`oid` null: a new file); the file as saved, or null when it was not.
98
+ async function commitFile(path: string, oid: string | null, text: string, message: string, change: string): Promise<RepoFile | null> {
80
99
  setSaving(true);
81
100
  env.notify({ text: t.common.saving });
82
101
  try {
83
- const oids = await env.commit(message, [{ path, content: text, oid: file.oid }], change);
102
+ const oids = await env.commit(message, [{ path, content: text, oid }], change);
84
103
  const saved = { path, oid: oids[path], text };
85
- setFile(saved);
86
104
  env.onSaved(saved);
87
105
  env.notify({ text: savedText(change), kind: 'saved' });
88
- return true;
106
+ return saved;
89
107
  } catch (e) {
90
108
  env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
91
- return false;
109
+ return null;
92
110
  } finally {
93
111
  setSaving(false);
94
112
  }
95
113
  }
96
114
 
115
+ async function save(text: string, message: string, change: string): Promise<boolean> {
116
+ if (!file) return false;
117
+ const saved = await commitFile(file.path, file.oid, text, message, change);
118
+ if (saved) setFile(saved);
119
+ return !!saved;
120
+ }
121
+
122
+ const settings = buildSettingsOf(build?.text);
123
+ const buildSwitches: { key: BuildSetting; label: string; hint: string; change: string; commit: (on: boolean) => string }[] = [
124
+ {
125
+ key: 'contentLane',
126
+ label: t.advanced.fastContentBuilds,
127
+ hint: t.advanced.fastContentBuildsHint,
128
+ change: t.app.changes.fastContentBuilds,
129
+ commit: siteT.advanced.fastContentBuildsCommit,
130
+ },
131
+ {
132
+ key: 'paidBuilds',
133
+ label: t.advanced.paidBuilds,
134
+ hint: t.advanced.paidBuildsHint,
135
+ change: t.app.changes.paidBuilds,
136
+ commit: siteT.advanced.paidBuildsCommit,
137
+ },
138
+ ];
139
+
140
+ // Paid builds are off until Cloudflare says the account has Workers Paid; on, they can always be turned off.
141
+ const paidLocked = (key: BuildSetting, on: boolean) => key === 'paidBuilds' && !on && paid !== 'yes';
142
+
143
+ async function turnBuild(key: BuildSetting, next: boolean, commit: (on: boolean) => string, change: string) {
144
+ if (build === undefined || (next && paidLocked(key, settings[key]))) return;
145
+ const saved = await commitFile(
146
+ BUILD_SETTINGS_PATH,
147
+ build?.oid ?? null,
148
+ withBuildSetting(build?.text, key, next),
149
+ commit(next),
150
+ t.app.changes[next ? 'turnedOn' : 'turnedOff'](change),
151
+ );
152
+ if (saved) setBuild(saved);
153
+ }
154
+
97
155
  async function turn(next: boolean) {
98
156
  if (!file || on === null || (next && !ready)) return;
99
157
  await save(
@@ -124,17 +182,14 @@ export default function Advanced({ env }: { env: Env }) {
124
182
  <ProblemNote>{t.advanced.noLine}</ProblemNote>
125
183
  ) : (
126
184
  <>
127
- <div className="mb-6">
128
- <FormField label={t.advanced.place} hint={t.advanced.placeHint}>
129
- <SegmentedButton
130
- label={t.advanced.place}
131
- value={jurisdiction ?? AUTO}
132
- options={places.map((p) => ({ value: p, label: p === AUTO ? t.advanced.placeAuto : jurisdictionName(p) }))}
133
- disabled={saving || on}
134
- onChange={place}
135
- />
136
- </FormField>
137
- </div>
185
+ <MenuRow
186
+ label={t.advanced.place}
187
+ hint={t.advanced.placeHint}
188
+ value={jurisdiction ?? AUTO}
189
+ options={places.map((p) => ({ value: p, label: p === AUTO ? t.advanced.placeAuto : jurisdictionName(p) }))}
190
+ disabled={saving || on}
191
+ onChange={place}
192
+ />
138
193
  <SwitchRow
139
194
  label={t.blockSettings.active}
140
195
  texts={[t.library.on, t.library.off]}
@@ -147,6 +202,32 @@ export default function Advanced({ env }: { env: Env }) {
147
202
  )}
148
203
  </div>
149
204
  </section>
205
+ <section className="mb-8">
206
+ <GroupTitle>{t.advanced.builds}</GroupTitle>
207
+ <div className="px-4">
208
+ {buildProblem ? (
209
+ <ProblemNote>{buildProblem}</ProblemNote>
210
+ ) : (
211
+ <>
212
+ {buildSwitches.map(({ key, label, hint, change, commit }) => (
213
+ <SwitchRow
214
+ key={key}
215
+ label={label}
216
+ hint={hint}
217
+ texts={[t.library.on, t.library.off]}
218
+ selected={settings[key]}
219
+ disabled={saving || build === undefined || paidLocked(key, settings[key])}
220
+ onChange={(next) => turnBuild(key, next, commit, change)}
221
+ />
222
+ ))}
223
+ {paid && !settings.paidBuilds && paid !== 'yes' && <SupportingText className="mt-2">{t.advanced.plan[paid]}</SupportingText>}
224
+ <LinkButton href={BILLING_URL} target="_blank" rel="noopener" kind="text" size="small" icon={ArrowSquareOut}>
225
+ {t.advanced.billing}
226
+ </LinkButton>
227
+ </>
228
+ )}
229
+ </div>
230
+ </section>
150
231
  </Screen>
151
232
  );
152
233
  }
package/src/App.tsx CHANGED
@@ -1,19 +1,19 @@
1
1
  // The editor at /editor/ (docs/editor.md): sign in with GitHub, pick a part of the site, edit and
2
- // save. The address after # says what is open, so reloading keeps it:
2
+ // save. The layout: the full-height navigation rail or drawer (MainMenu.tsx) with, beside it, the top bar (topBarSlot.tsx) over the screen. The address after # says what is open, so reloading keeps it:
3
3
  // (empty) the start page
4
4
  // liste/<id> the list of a part, e.g. liste/priser, and liste/<id>/<app> a tab an installed app adds to it
5
5
  // (AppDefinition.tabs): liste/cookies/cookiescanner
6
6
  // header what is in the header on every page (the menu)
7
7
  // lister the short lists (the testimonials, the apps' lists)
8
- // indstillinger the settings of the whole site (Library.tsx Settings), opened by the gear in the top bar:
9
- // indstillinger/appstore the app store, its apps, and indstillinger/appstore/temaer its themes (AppList.tsx StorePage)
8
+ // indstillinger/appstore the app store, its apps, and indstillinger/appstore/temaer its themes (AppList.tsx StorePage); the
9
+ // other settings are screens of their own, in the drawer's last section (MainMenu.tsx)
10
10
  // ny/<id> a new entry, ny/<id>/<kind> one of a kind (a pop-up: standard, or an app's)
11
11
  // virksomheden the business, and virksomheden/<part> its parts: kontakt, betaling; and
12
12
  // virksomheden/betaling/<part> its own parts: mobilepay, bank
13
13
  // <screen> a screen with parts of the site's own (partScreens), and <screen>/<part> its parts:
14
14
  // privatliv/cookiebanner, privatliv/privatlivssider, or one of the editor's own screens (Part.opens):
15
15
  // privatliv/samtykker the visitors' answers in the cookie banner (Consents.tsx); and the settings of the apps installed with
16
- // its category: maaling/google-analytics, promovering/calendly
16
+ // its category: maaling/google-analytics
17
17
  // filer the uploaded pictures, PDFs, videos and zip files (Media.tsx)
18
18
  // seo "Søgemaskineoptimering" (Library.tsx Seo): seo/videresendelser the redirects' two lists
19
19
  // (Redirects.tsx): seo/videresendelser/alle the redirects, and seo/videresendelser/ikke-fundet
@@ -138,10 +138,8 @@ import {
138
138
  List,
139
139
  Lists,
140
140
  PartsHome,
141
- partsIn,
142
141
  PartsScreen,
143
142
  Seo,
144
- Settings,
145
143
  Start,
146
144
  Theme,
147
145
  ThemePage,
@@ -150,24 +148,14 @@ import {
150
148
  MotionSchemes,
151
149
  motionOf,
152
150
  } from './Library';
153
- import {
154
- appearance,
155
- asWritten,
156
- blockParts,
157
- collections,
158
- companyParts,
159
- groups,
160
- icons,
161
- listRowsOf,
162
- lists,
163
- partScreens,
164
- seoParts,
165
- } from './site';
166
- import { config, header, relationsOf, type SiteData, siteName, siteOf, testimonialsOf, usedBy, whatIsAt } from '@site/editor';
151
+ import { appearance, asWritten, blockParts, collections, companyParts, listRowsOf, partScreens, seoParts } from './site';
152
+ import { config, relationsOf, type SiteData, siteName, siteOf, testimonialsOf, usedBy, whatIsAt } from '@site/editor';
167
153
  import { themes } from './themes';
168
154
  import Screen from './Screen';
169
- import { Button, EmptyNote, NavigationRail, NavigationRailItem, NavigationRailSection, ProblemNote, startRipples } from './ui';
170
- import { setSidebar, useSidebar } from './sidebar';
155
+ import { Button, EmptyNote, ProblemNote, startRipples } from './ui';
156
+ import { useMenuOpen } from './menuOpen';
157
+ import { MainMenu } from './MainMenu';
158
+ import { TopBarLayout } from './topBarSlot';
171
159
  import { Ask, ask } from './ask';
172
160
  import { LinkAsk } from './LinkSheet';
173
161
  import { type ListRows, type Option, type TestimonialRow } from './fields';
@@ -285,6 +273,10 @@ const MOVED: Record<string, string> = {
285
273
  videresendelser: 'seo/videresendelser',
286
274
  'videresendelser/alle': 'seo/videresendelser/alle',
287
275
  'videresendelser/ikke-fundet': 'seo/videresendelser/ikke-fundet',
276
+ // The settings of the booking and newsletter apps were on a screen "Promovering"; they are on the apps' own pages now.
277
+ promovering: 'apps',
278
+ 'promovering/calendly': 'apps/calendly/indstillinger',
279
+ 'promovering/mailchimp': 'apps/mailchimp/indstillinger',
288
280
  [STORE_BEFORE]: STORE_ROUTE,
289
281
  [`${STORE_BEFORE}/temaer`]: STORE_THEMES,
290
282
  };
@@ -317,11 +309,6 @@ function Editor() {
317
309
  // Whether someone is signed in (the Worker keeps the sign-in, packages/editor/worker/session.js): null until the Worker or this
318
310
  // browser's copy of the files (cache.ts) has said.
319
311
  const [signedIn, setSignedIn] = useState<boolean | null>(null);
320
- // Whether the sidebar is open on a computer (sidebar.ts).
321
- const sidebar = useSidebar();
322
- // While it is closed: the mouse over the rail, which then shows the whole sidebar over the content.
323
- const [peek, setPeek] = useState(false);
324
- const peekTimer = useRef(0);
325
312
  const [user, setUser] = useState<Viewer | null>(null);
326
313
  const [files, setFiles] = useState<RepoFile[] | null>(null);
327
314
  const [error, setError] = useState('');
@@ -329,10 +316,11 @@ function Editor() {
329
316
  // Counts the visits to an address: a form starts fresh on each, but not when a new entry that was
330
317
  // just saved gets its own address.
331
318
  const [visit, setVisit] = useState(0);
332
- // The messages at the bottom stand in the middle of the content, next to the sidebar (editor.css).
319
+ // The messages at the bottom and the save bar stand next to the standard drawer while it is open (editor.css).
320
+ const { open: menuOpen } = useMenuOpen();
333
321
  useEffect(() => {
334
- document.documentElement.dataset.sidebar = sidebar ? 'open' : 'closed';
335
- }, [sidebar]);
322
+ document.documentElement.dataset.sidebar = menuOpen ? 'open' : 'closed';
323
+ }, [menuOpen]);
336
324
  // Stops waiting for the last save to be on the site.
337
325
  const stopWaiting = useRef<() => void>(undefined);
338
326
  // What the saves the editor waits for the website to have changed (save).
@@ -679,97 +667,35 @@ function Editor() {
679
667
  );
680
668
  }
681
669
 
682
- // The part of the site that is open, for the sidebar: the menu under "Header", the lists and testimonials under
683
- // "Lister", the front page under "Udseende".
684
- const open = collectionOf(collections, route)?.collection;
685
- const part = open?.id || route.replace(/^(liste|ny|slettet)\/([^/]+).*/, '$2');
686
- const current = lists.includes(part)
687
- ? 'lister'
688
- : header.includes(part)
689
- ? 'header'
690
- : part.startsWith('virksomheden')
691
- ? 'virksomheden'
692
- : part.startsWith('blokke')
693
- ? 'blokke'
694
- : part.startsWith('udseende')
695
- ? 'udseende'
696
- : part.startsWith('seo')
697
- ? 'seo'
698
- : part.startsWith('apps')
699
- ? 'apps'
700
- : (partScreens.find((s) => part.startsWith(`${s.id}/`))?.id ?? part);
701
-
702
670
  return (
703
671
  <>
704
- <div className="md:flex">
705
- {/* On a computer: the parts of the site in the theme's navigation rail, expanded and collapsed with its menu
706
- button; under it, while expanded, the logo and the name, which go to the start screen (the account is in the
707
- top bar, Screen.tsx). Collapsed, the mouse over it shows it expanded over the content for a moment (a short wait
708
- first, so passing over it does not), until the mouse leaves. On a phone, the start screen is the menu. */}
709
- <aside
710
- className="sticky top-0 z-30 hidden h-dvh shrink-0 pt-[env(safe-area-inset-top)] md:flex"
711
- onPointerEnter={(e) => {
712
- if (sidebar || e.pointerType !== 'mouse') return;
713
- peekTimer.current = window.setTimeout(() => setPeek(true), 150);
714
- }}
715
- onPointerLeave={() => {
716
- clearTimeout(peekTimer.current);
717
- setPeek(false);
718
- }}
719
- >
720
- <NavigationRail
721
- label={t.library.sidebar}
722
- menuLabel={sidebar ? t.library.closeSidebar : t.library.openSidebar}
723
- expanded={sidebar}
724
- peek={peek}
725
- onExpandedChange={(open) => {
726
- setPeek(false);
727
- setSidebar(open);
728
- }}
729
- header={
730
- <a href="#" className="flex min-w-0 items-center gap-3 font-semibold">
731
- <img src="/editor/icon-192.png" alt="" className="size-7 shrink-0 rounded-lg" />
732
- <span className="truncate">{siteName}</span>
733
- </a>
734
- }
735
- >
736
- {groups.map((g) => (
737
- <NavigationRailSection key={g.title} label={g.title}>
738
- {partsIn(g.ids, files ?? []).map(({ id, label, href, count }) => (
739
- <NavigationRailItem
740
- key={id}
741
- href={href}
742
- icon={icons[id]}
743
- label={label}
744
- count={files ? count : null}
745
- selected={current === id}
746
- />
747
- ))}
748
- </NavigationRailSection>
749
- ))}
750
- </NavigationRail>
751
- </aside>
752
-
753
- <div className="min-w-0 flex-1">
754
- {files ? (
755
- <>
756
- {user && !user.canSave && (
757
- <p className="bg-accent px-4 py-3 pt-[calc(0.75rem+env(safe-area-inset-top))] text-center text-sm text-on-accent">
758
- {t.app.cannotSave(user.login)}
759
- </p>
760
- )}
672
+ <TopBarLayout
673
+ aside={<MainMenu route={route} files={files} />}
674
+ above={
675
+ files &&
676
+ user &&
677
+ !user.canSave && (
678
+ <p className="bg-accent px-4 py-3 pt-[calc(0.75rem+env(safe-area-inset-top))] text-center text-sm text-on-accent">
679
+ {t.app.cannotSave(user.login)}
680
+ </p>
681
+ )
682
+ }
683
+ >
684
+ <>
685
+ <div className="editor-content min-w-0 flex-1">
686
+ {files ? (
761
687
  <Page key={visit} route={route} files={files} env={env} user={user} builtAt={builtAt} onLogout={signOut} />
762
- </>
763
- ) : (
764
- <Screen title={siteName}>
765
- <EmptyNote busy={!error}>{error || t.app.loading}</EmptyNote>
766
- </Screen>
767
- )}
768
- </div>
769
-
770
- <LinkAsk groups={linkGroups} />
771
- <Ask />
772
- </div>
688
+ ) : (
689
+ <Screen title={siteName}>
690
+ <EmptyNote busy={!error}>{error || t.app.loading}</EmptyNote>
691
+ </Screen>
692
+ )}
693
+ </div>
694
+
695
+ <LinkAsk groups={linkGroups} />
696
+ <Ask />
697
+ </>
698
+ </TopBarLayout>
773
699
  </>
774
700
  );
775
701
  }
@@ -809,15 +735,15 @@ function formAt(route: string, files: RepoFile[]): { part: Part; at: string; pag
809
735
  type PageProps = { route: string; files: RepoFile[]; env: Env; user: Viewer | null; builtAt: Date | null; onLogout: () => void };
810
736
 
811
737
  function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
812
- if (!route) return <Start files={files} builtAt={builtAt} />;
738
+ if (!route) return <Start builtAt={builtAt} />;
813
739
  if (route === 'konto') return <Account user={user} files={files} onLogout={onLogout} />;
814
740
  if (route === 'notifikationer') return <Notifications />;
815
- if (route === 'indstillinger') return <Settings files={files} />;
816
741
  if (route === 'header') return <Header files={files} />;
817
742
  if (route === 'lister') return <Lists files={files} />;
818
743
  if (route === 'virksomheden') return <Company files={files} />;
819
744
  if (route === 'blokke') return <Blocks files={files} />;
820
- if (route === 'filer') return <Media files={files} env={env} />;
745
+ if (route === 'filer' || route.startsWith('filer/'))
746
+ return <Media key={route} files={files} env={env} open={route === 'filer' ? undefined : '/' + route.slice('filer/'.length)} />;
821
747
  if (route === 'seo') return <Seo />;
822
748
  if (route === REDIRECTS_ROUTE) return <Redirects env={env} />;
823
749
  if (route === SEO_SCORES_ROUTE) return <SeoScores files={files} env={env} />;
package/src/AppList.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  // "Indstillinger" › "Apps" (#apps): the apps installed (src/apps/, apps.ts: the site's own and those from the app store,
2
- // with one from the store not yet built as "Installeres …"), each with who made it under its name and, from the store,
2
+ // with one from the store not yet built as "Installeres…"), each with who made it under its name and, from the store,
3
3
  // that it has a newer version, with search (by name, who made it and what it does), the order (A–Å, Å–A, kept in this
4
4
  // browser) and headings by the first letter, as the lists under "Indhold" have. "Indstillinger" › "Appstore"
5
5
  // (#indstillinger/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
@@ -291,7 +291,7 @@ function ButtonParts() {
291
291
  * prefix and a suffix, the error's icon, a password with its button, a counter, read-only, and several lines (growing,
292
292
  * and three rows); and a checkbox empty, ticked, with a line, disabled, indeterminate and wrong. */
293
293
  function FieldParts() {
294
- const fields = 'grid gap-6 sm:grid-cols-2';
294
+ const fields = 'grid gap-6 medium:grid-cols-2';
295
295
  return (
296
296
  <>
297
297
  <Part name="TextField" use={screens.textField}>
@@ -380,7 +380,7 @@ function CardParts() {
380
380
  <>
381
381
  <Part name="Card" use={say.card}>
382
382
  <Preview>
383
- <div className="grid gap-4 p-4 sm:grid-cols-2">
383
+ <div className="grid gap-4 p-4 medium:grid-cols-2">
384
384
  {cardKinds.map((k) => (
385
385
  <SiteCard key={k} kind={k}>
386
386
  <p className="font-medium">{say.kinds[k]}</p>
@@ -402,7 +402,7 @@ function CardParts() {
402
402
  </Part>
403
403
  <Part name="List" use={say.list}>
404
404
  <Preview>
405
- <div className="grid items-start gap-4 p-4 lg:grid-cols-2">
405
+ <div className="grid items-start gap-4 p-4 expanded:grid-cols-2">
406
406
  <SiteCard>
407
407
  <SiteList>
408
408
  <SiteListItem headline={one} />
package/src/Colors.tsx CHANGED
@@ -359,7 +359,7 @@ export default function Colors({ env, files, fallback }: { env: Env; files: Repo
359
359
  first={preview(mode)}
360
360
  main={
361
361
  <>
362
- <div className="grid gap-5 sm:grid-cols-2">
362
+ <div className="grid gap-5 medium:grid-cols-2">
363
363
  {colorFields.map((field) => {
364
364
  const id = `color-${mode}-${field.name}`;
365
365
  return (
@@ -66,7 +66,7 @@ export default function CommunicationParts() {
66
66
  </div>
67
67
  </Example>
68
68
  </div>
69
- <SiteNavigationDrawer className="px-3 py-4">
69
+ <SiteNavigationDrawer>
70
70
  <SiteNavigationDrawerRow>
71
71
  <SiteNavigationDrawerItem href="">{say.home}</SiteNavigationDrawerItem>
72
72
  </SiteNavigationDrawerRow>