@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
package/src/Library.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // The start screen of the editor (the parts of the site in groups, like a phone's settings) and the
1
+ // The start screen of the editor (empty but for its title, status and FAB) and the
2
2
  // list of each part: blog posts newest first, the rest by name, prices and testimonials in their order
3
3
  // on the site (with arrows to move them), and a search. Lists that are not in an order of their own can
4
4
  // be sorted another way (a list of kinds, by its kinds too); the choice is kept in this browser. Pages
@@ -15,7 +15,6 @@ import {
15
15
  ClockCounterClockwise,
16
16
  CircleHalf,
17
17
  FileText,
18
- Gauge,
19
18
  GithubLogo,
20
19
  GoogleLogo,
21
20
  List as ListIcon,
@@ -33,6 +32,7 @@ import { byText, entryData, type Collection, type Part, type PartScreen } from '
33
32
  import { config, countsOf, header, relationsOf, siteName } from '@site/editor';
34
33
  import { iconPack, iconPacks } from './iconPacks';
35
34
  import { theme, themes, type ThemeInfo } from './themes';
35
+ import { StartFab } from './StartFab';
36
36
  import { StartStatus } from './StartStatus';
37
37
  import {
38
38
  appearance,
@@ -41,13 +41,11 @@ import {
41
41
  blockParts,
42
42
  collections,
43
43
  companyParts,
44
- groups,
45
44
  icons,
46
45
  lists,
47
46
  listsShown,
48
47
  partScreens,
49
48
  seoParts,
50
- settings,
51
49
  } from './site';
52
50
  import { readFields, splitFrontmatter } from './frontmatter.mjs';
53
51
  import { ConflictError, type RepoFile, type Viewer } from './github';
@@ -108,6 +106,7 @@ import {
108
106
  useThemeCatalog,
109
107
  } from './ThemeStore';
110
108
  import { STANDARD_THEME, STORE_ROUTE } from './store';
109
+ import { REDIRECTS_ROUTE } from './Redirects';
111
110
  import { MOTION_SCHEMES, schemeInUse, type MotionScheme } from '../lib/motion';
112
111
  import { applyMotion } from './theme';
113
112
  import { colorsOf } from './Colors';
@@ -118,12 +117,12 @@ import { offBlocks } from './registry';
118
117
  import { sideChoicesOf, sideSettingOf } from './SideColumn';
119
118
 
120
119
  // Where a part opens: its list, or the file itself when it is one file (the menu).
121
- const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
120
+ export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
122
121
 
123
122
  const countOf = (c: Collection, files: RepoFile[]): number =>
124
123
  c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
125
124
 
126
- // A part's name, where it opens, and how many entries it has (null for one file).
125
+ // A part's name, where it opens, and how many entries it has (null for one file): for the main drawer (MainMenu.tsx).
127
126
  export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
128
127
  const { parts } = t.common;
129
128
  if (id === 'virksomheden') return { label: parts.company, href: '#virksomheden', count: null };
@@ -135,6 +134,26 @@ export function partOf(id: string, files: RepoFile[]): { label: string; href: st
135
134
  if (id === 'blokvalg') return { label: parts.blockSettings, href: '#blokvalg', count: null };
136
135
  if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
137
136
  if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
137
+ if (id === 'videresendelser') return { label: parts.redirects, href: `#${REDIRECTS_ROUTE}`, count: null };
138
+ const seoPart = seoParts.find((p) => p.id === id);
139
+ if (seoPart) return { label: seoPart.label, href: `#seo/${id}`, count: null };
140
+ // The screens under "Udseende" (Theme): the ones the editor has, then the site's own (appearance).
141
+ const appearanceLabel = {
142
+ temaer: parts.themes,
143
+ ikonpakker: parts.iconPacks,
144
+ bevaegelse: parts.motion,
145
+ sidekolonne: parts.sideColumn,
146
+ farver: parts.colors,
147
+ logo: parts.brand,
148
+ dele: parts.editorParts,
149
+ }[id];
150
+ const appearancePart = appearanceLabel ?? appearance.find((p) => p.id === id)?.label;
151
+ if (appearancePart) return { label: appearancePart, href: `#udseende/${id}`, count: null };
152
+ // The parts of a screen with parts ("Privatliv"): where each opens, or its screen's address with its id.
153
+ for (const screen of partScreens) {
154
+ const own = screen.parts.find((p) => p.id === id);
155
+ if (own) return { label: own.label, href: own.href ?? `#${screen.id}/${id}`, count: null };
156
+ }
138
157
  if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
139
158
  if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
140
159
  if (id === 'licenser') return { label: parts.licenses, href: '#licenser', count: null };
@@ -145,7 +164,7 @@ export function partOf(id: string, files: RepoFile[]): { label: string; href: st
145
164
  return { label: c.label, href: hrefOf(c), count: c.kind === 'files' && c.files.length === 1 ? null : countOf(c, files) };
146
165
  }
147
166
 
148
- /** The parts of a group on the start screen (and in the sidebar), A–Å by their names in the editor's language. */
167
+ /** The parts of a group on a screen with parts (Parts), A–Å by their names in the editor's language. */
149
168
  export const partsIn = (ids: string[], files: RepoFile[]) =>
150
169
  ids.map((id) => ({ id, ...partOf(id, files) })).sort((a, b) => byText(a.label, b.label));
151
170
 
@@ -156,41 +175,18 @@ function PartRow({ id, label, href, count }: { id: string } & ReturnType<typeof
156
175
  return <ListItem href={href} icon={icons[id]} headline={label} count={count} arrow />;
157
176
  }
158
177
 
159
- // The start screen, with the CMS version and when the site was last updated (StartStatus.tsx) under the title;
160
- // on a computer the groups two side by side.
161
- export function Start({ files, builtAt }: { files: RepoFile[]; builtAt: Date | null }) {
178
+ // The start screen: the site's name, the CMS version and when the site was last updated (built on Cloudflare, App.tsx), as StartStatus.tsx shows them, under it, and the FAB
179
+ // menu for a new page or post (StartFab.tsx). The parts of the site are in the main drawer (MainMenu.tsx); the rest is left empty for now.
180
+ export function Start({ builtAt }: { builtAt: Date | null }) {
162
181
  return (
163
182
  <Screen
164
183
  title={siteName}
165
184
  wide
166
185
  status={<StartStatus builtAt={builtAt} />}
167
- // The notifications, the settings and the account in the top bar, on a phone too (Screen.tsx).
186
+ // The notifications and the account in the top bar, on a phone too (Screen.tsx).
168
187
  home
169
188
  >
170
- <div className="md:grid md:grid-cols-2 md:items-start md:gap-x-6">
171
- {groups.map((g) => (
172
- <Group key={g.title} title={g.title}>
173
- {partsIn(g.ids, files).map((p) => (
174
- <PartRow key={p.id} {...p} />
175
- ))}
176
- </Group>
177
- ))}
178
- </div>
179
- </Screen>
180
- );
181
- }
182
-
183
- /** "Indstillinger" (#indstillinger), which the gear in the top bar opens (Screen.tsx SettingsLink): the settings of the
184
- * whole site (settings in packages/editor/defaults/editor.ts: "Analytics", "Apps", "Privatliv", "Promovering", "Søgemaskineoptimering",
185
- * "Udseende"), A–Å as a group of the start screen, each going back here (backFrom). */
186
- export function Settings({ files }: { files: RepoFile[] }) {
187
- return (
188
- <Screen title={t.common.parts.settings} wide back={{ href: '#', label: siteName }}>
189
- <Group>
190
- {partsIn(settings, files).map((p) => (
191
- <PartRow key={p.id} {...p} />
192
- ))}
193
- </Group>
189
+ <StartFab />
194
190
  </Screen>
195
191
  );
196
192
  }
@@ -264,7 +260,7 @@ function themeStatus(id: string, files: RepoFile[]): string | undefined {
264
260
  }
265
261
 
266
262
  /** "Temaer" (#udseende/temaer): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
267
- * does not have yet, as "Installeres …"), by name, each with its picture, its version and who made it, and whether it is
263
+ * does not have yet, as "Installeres…"), by name, each with its picture, its version and who made it, and whether it is
268
264
  * in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
269
265
  export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
270
266
  const catalog = useThemeCatalog(env);
@@ -528,15 +524,11 @@ export function PartsHome() {
528
524
  return <Parts title={t.common.parts.editorParts} route="udseende/dele" parts={parts} back={backToAppearance} />;
529
525
  }
530
526
 
531
- /** "Søgemaskineoptimering" (#seo) under Indstillinger: "SEO-vurdering" (SeoScores.tsx, #seo/vurdering), "Videresendelser"
532
- * (Redirects.tsx, #seo/videresendelser) and then the site's own parts (seoParts: "Interne links"), each on a screen of
533
- * its own (CompanyForm.tsx). */
527
+ /** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/videresendelser) and then
528
+ * the site's own parts (seoParts: "Interne links"), each on a screen of its own (CompanyForm.tsx). "SEO-vurdering"
529
+ * (SeoScores.tsx, #seo/vurdering) is hidden: nothing links to it. */
534
530
  export function Seo() {
535
- const parts: Part[] = [
536
- { id: 'vurdering', label: t.seo.title, icon: Gauge },
537
- { id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser },
538
- ...seoParts,
539
- ];
531
+ const parts: Part[] = [{ id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser }, ...seoParts];
540
532
  return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
541
533
  }
542
534
 
@@ -1342,16 +1334,19 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1342
1334
  />
1343
1335
  );
1344
1336
  };
1337
+ const privacyScreen = partScreens.find((s) => s.id === 'privatliv');
1345
1338
  return (
1346
1339
  <Screen
1347
1340
  title={collection.label}
1348
1341
  wide
1349
1342
  back={
1350
- lists.includes(collection.id)
1351
- ? { href: '#lister', label: t.common.parts.lists }
1352
- : header.includes(collection.id)
1353
- ? { href: '#header', label: t.common.parts.header }
1354
- : { href: '#', label: siteName }
1343
+ collection.id === config.lists.cookies && privacyScreen
1344
+ ? { href: `#${privacyScreen.id}`, label: privacyScreen.label }
1345
+ : lists.includes(collection.id)
1346
+ ? { href: '#lister', label: t.common.parts.lists }
1347
+ : header.includes(collection.id)
1348
+ ? { href: '#header', label: t.common.parts.header }
1349
+ : { href: '#', label: siteName }
1355
1350
  }
1356
1351
  tabs={
1357
1352
  appTabs.length > 0
@@ -1491,8 +1486,8 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1491
1486
  function Under({ item, address, children }: { item: Item; address?: boolean; children: ReactNode }) {
1492
1487
  return (
1493
1488
  <span className="flex gap-6">
1494
- {address && <span className="hidden min-w-0 flex-1 truncate md:block">{item.path}</span>}
1495
- <span className="min-w-0 truncate md:ml-auto md:shrink-0 md:text-right">{children}</span>
1489
+ {address && <span className="hidden min-w-0 flex-1 truncate expanded:block">{item.path}</span>}
1490
+ <span className="min-w-0 truncate expanded:ml-auto expanded:shrink-0 expanded:text-right">{children}</span>
1496
1491
  </span>
1497
1492
  );
1498
1493
  }
package/src/Licenses.tsx CHANGED
@@ -1,7 +1,8 @@
1
1
  // "Licenser" (#licenser), under "Indstillinger": the third-party work the site and the editor are made with, as the
2
2
  // site was last built (/editor/licenses.json, licensesIntegration in packages/editor/lib/licenses.mjs), with a search by
3
3
  // name, who made it and the licence's name. First the icon packs (the system's, each with the sets it takes icons from
4
- // besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded),
4
+ // besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded,
5
+ // the CMS's own licence "Morpheme CMS itself" first, with its version),
5
6
  // then each theme the site is built with and each app installed, under its name: its own licence first, then what it
6
7
  // brings (what it declares, its icon packs and what is shipped through them alone). Each row has the work's name, its
7
8
  // version, licence and who made it, and what it is and where it is used; one with a text opens it
@@ -15,9 +16,12 @@ import { backFrom } from './site';
15
16
  import { apps } from './registry';
16
17
  import { themes } from './themes';
17
18
  import { byText } from './schema';
19
+ import { PACKAGE } from '../lib/cms-version.mjs';
18
20
  import { locale, t } from './i18n';
19
21
  import {
22
+ CMS_NAME,
20
23
  loadLicenses,
24
+ splitCms,
21
25
  UNLICENSED,
22
26
  type LicensedWork,
23
27
  type LicenseKind,
@@ -115,18 +119,20 @@ function WorkRow({ work, owners, places, open = true }: { work: LicensedWork; ow
115
119
  );
116
120
  }
117
121
 
118
- /** An app's or a theme's own licence, first in its group. */
122
+ /** An app's, a theme's or the CMS's own licence (`name`: whose), first in its group. */
119
123
  function OwnRow({
120
124
  owner,
125
+ name,
121
126
  open = true,
122
127
  }: {
123
128
  open?: boolean;
124
- owner: Pick<LicenseOwner, 'id' | 'kind' | 'key' | 'name' | 'author' | 'license' | 'version'> & { text?: string | null };
129
+ name: string;
130
+ owner: Pick<LicenseOwner, 'author' | 'license' | 'version'> & { id: string; text?: string | null };
125
131
  }) {
126
132
  return (
127
133
  <LicenseRow
128
134
  icon={Certificate}
129
- name={t.licenses.own(ownerName(owner))}
135
+ name={t.licenses.own(name)}
130
136
  lines={[byline(owner.license, owner.author)]}
131
137
  version={owner.version}
132
138
  href={open && owner.text ? hrefOf(owner.id) : undefined}
@@ -165,18 +171,23 @@ export default function Licenses() {
165
171
  const owners = file?.owners ?? [];
166
172
  // The system's icon packs (a theme's own are under it, `where` it).
167
173
  const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('tema/')))) : [];
168
- const system = file ? sorted(file.works.filter((w) => w.kind !== 'icons' && w.where.some((x) => x === 'editor' || x === 'site'))) : [];
174
+ // What the editor and the site bring: the CMS's own licence first, the rest A–Å.
175
+ const { cms, rest: system } = splitCms(
176
+ file ? sorted(file.works.filter((w) => w.kind !== 'icons' && w.where.some((x) => x === 'editor' || x === 'site'))) : [],
177
+ );
178
+ const ownCms = cms && (fits(cms) || matches(CMS_NAME)) ? cms : null;
169
179
  // The themes the site is built with, then the apps installed, each A–Å.
170
180
  const inUse = owners
171
181
  .filter((o) => o.inUse)
172
182
  .sort((a, b) => (a.kind === b.kind ? byText(ownerName(a), ownerName(b)) : a.kind === 'theme' ? -1 : 1));
173
183
  const groups = [
174
- { key: 'icons', title: t.licenses.icons, own: null, works: packs.filter(fits) },
175
- { key: 'system', title: t.licenses.system, own: null, works: system.filter(fits) },
184
+ { key: 'icons', title: t.licenses.icons, own: null, ownName: '', works: packs.filter(fits) },
185
+ { key: 'system', title: t.licenses.system, own: ownCms, ownName: CMS_NAME, works: system.filter(fits) },
176
186
  ...inUse.map((o) => ({
177
187
  key: o.id,
178
188
  title: o.kind === 'app' ? t.licenses.app(ownerName(o)) : t.licenses.theme(ownerName(o)),
179
189
  own: matches(ownerName(o), o.author, o.license, licenseName(o.license)) ? o : null,
190
+ ownName: ownerName(o),
180
191
  works: file ? worksOf(o, file).filter(fits) : [],
181
192
  })),
182
193
  ].filter((g) => g.own || g.works.length);
@@ -193,7 +204,7 @@ export default function Licenses() {
193
204
  {groups.length ? (
194
205
  groups.map((g) => (
195
206
  <Group key={g.key} title={g.title}>
196
- {g.own && <OwnRow owner={g.own} />}
207
+ {g.own && <OwnRow owner={g.own} name={g.ownName} />}
197
208
  {g.works.map((w) => (
198
209
  <WorkRow key={w.id} work={w} owners={owners} places={g.key === 'system' || g.key === 'icons'} />
199
210
  ))}
@@ -228,7 +239,8 @@ export function LicensePage({ id }: { id: string }) {
228
239
  const file = useLicenses();
229
240
  const work = file?.works.find((w) => w.id === id);
230
241
  const owner = file?.owners.find((o) => o.id === id);
231
- const name = work?.name ?? (owner ? t.licenses.own(ownerName(owner)) : t.common.parts.licenses);
242
+ const isCms = work?.id === PACKAGE;
243
+ const name = isCms ? t.licenses.own(CMS_NAME) : (work?.name ?? (owner ? t.licenses.own(ownerName(owner)) : t.common.parts.licenses));
232
244
  const text = work?.text ?? owner?.text;
233
245
  const back = { href: `#${LICENSES_ROUTE}`, label: t.common.parts.licenses };
234
246
  return (
@@ -240,7 +252,11 @@ export function LicensePage({ id }: { id: string }) {
240
252
  ) : (
241
253
  <>
242
254
  <Group>
243
- {work ? <WorkRow work={work} owners={file?.owners ?? []} places open={false} /> : <OwnRow owner={owner!} open={false} />}
255
+ {work && !isCms ? (
256
+ <WorkRow work={work} owners={file?.owners ?? []} places open={false} />
257
+ ) : (
258
+ <OwnRow owner={(work ?? owner)!} name={isCms ? CMS_NAME : ownerName(owner!)} open={false} />
259
+ )}
244
260
  </Group>
245
261
  <section className="mb-8">
246
262
  <GroupTitle>{t.licenses.text}</GroupTitle>
@@ -292,7 +308,7 @@ export function OwnerLicenses({
292
308
  };
293
309
  return (
294
310
  <Group title={t.common.parts.licenses}>
295
- <OwnRow owner={self} />
311
+ <OwnRow owner={self} name={ownerName(self)} />
296
312
  {owner && file
297
313
  ? worksOf(owner, file).map((w) => <WorkRow key={w.id} work={w} owners={file.owners} places={false} />)
298
314
  : (declared.thirdParty ?? []).map((x, i) =>
@@ -0,0 +1,60 @@
1
+ // The main drawer and its menu button on #udseende/dele/editor (Parts.tsx): the modal drawer in its two levels (sections, and
2
+ // a section's list with the back row and a fold-out), opened from a button, so it can be tried at any width, and the menu
3
+ // button open and closed. Exposes MainDrawerSample. Uses MainDrawer and its parts from ui.tsx.
4
+ import { useState } from 'react';
5
+ import { icons } from './site';
6
+ import { t } from './i18n';
7
+ import {
8
+ Button,
9
+ MainDrawer,
10
+ MainDrawerBack,
11
+ MainDrawerFold,
12
+ MainDrawerItem,
13
+ MainDrawerLevel,
14
+ MainDrawerSectionItem,
15
+ MainDrawerSubitem,
16
+ MenuButton,
17
+ } from './ui';
18
+
19
+ const { parts } = t.common;
20
+
21
+ export function MainDrawerSample() {
22
+ const [open, setOpen] = useState(false);
23
+ const [section, setSection] = useState<string | null>(null);
24
+ return (
25
+ <div className="flex flex-wrap items-center gap-3">
26
+ <Button kind="outlined" onClick={() => setOpen(true)}>
27
+ {t.library.openMenu}
28
+ </Button>
29
+ <MenuButton label={t.library.openMenu} onClick={() => setOpen(true)} />
30
+ <MenuButton label={t.library.closeMenu} open onClick={() => setOpen(false)} />
31
+ <MainDrawer
32
+ label={t.library.navigation}
33
+ open={open}
34
+ onOpenChange={setOpen}
35
+ onEscape={() => {
36
+ const inList = section !== null;
37
+ setSection(null);
38
+ return inList;
39
+ }}
40
+ variant="modal"
41
+ >
42
+ {section === null ? (
43
+ <MainDrawerLevel>
44
+ <MainDrawerItem href="" icon={icons.home} label={parts.company} />
45
+ <MainDrawerSectionItem icon={icons.globalt} label={parts.header} selected onClick={() => setSection('globalt')} />
46
+ <MainDrawerSectionItem icon={icons.udseende} label={parts.appearance} onClick={() => setSection('udseende')} />
47
+ </MainDrawerLevel>
48
+ ) : (
49
+ <MainDrawerLevel focus="list">
50
+ <MainDrawerBack label={t.library.mainMenu} onClick={() => setSection(null)} />
51
+ <MainDrawerFold href="" icon={icons.header} label={parts.header} open>
52
+ <MainDrawerSubitem href="" label={parts.apps} selected />
53
+ </MainDrawerFold>
54
+ <MainDrawerItem href="" icon={icons.seo} label={parts.seo} count={3} />
55
+ </MainDrawerLevel>
56
+ )}
57
+ </MainDrawer>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,136 @@
1
+ // The editor's main menu (App.tsx renders it): the drawer (ui.tsx MainDrawer) in two levels. The first lists Home and the
2
+ // sections; a section opens its list, under a back row, with the open screen marked and its fold-out folded out. Apps lists
3
+ // Appstore and a row per app installed. From medium and wider the first level is also the collapsed navigation rail
4
+ // (MainRailItem), whose sections open the drawer at their list. Exposes MainMenu. The tree and the open place are lib/menu.mjs
5
+ // (menuTree, currentPlace); it only wires them to the drawer.
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+ import { config, header } from '@site/editor';
8
+ import { currentPlace, menuTree } from '../lib/menu.mjs';
9
+ import { STORE_ROUTE } from './store';
10
+ import { collectionOf } from './schema';
11
+ import { apps, collections, groups, icons, lists, partScreens } from './site';
12
+ import { partOf } from './Library';
13
+ import { childrenOf } from './menuChildren';
14
+ import { t } from './i18n';
15
+ import { useMenuOpen } from './menuOpen';
16
+ import {
17
+ MainDrawer,
18
+ MainDrawerBack,
19
+ MainDrawerFold,
20
+ MainDrawerItem,
21
+ MainDrawerLevel,
22
+ MainDrawerSectionItem,
23
+ MainDrawerSubitem,
24
+ MainRailItem,
25
+ MainRailSectionItem,
26
+ } from './ui';
27
+ import type { RepoFile } from './github';
28
+
29
+ export function MainMenu({ route, files }: { route: string; files: RepoFile[] | null }) {
30
+ const { open, setOpen } = useMenuOpen();
31
+ const tree = useMemo(
32
+ () =>
33
+ menuTree(
34
+ groups,
35
+ (id: string) => partOf(id, files ?? []),
36
+ (id: string) => childrenOf(id, files ?? []),
37
+ ),
38
+ [files],
39
+ );
40
+ // A file being edited (a page, a post) is open under its list; the same call as wave 3's in App.tsx.
41
+ const collection = collectionOf(collections, route)?.collection;
42
+ const place = currentPlace(route, tree, {
43
+ open: collection?.id,
44
+ lists,
45
+ header,
46
+ partScreens: partScreens.map((s) => s.id),
47
+ cookies: config.lists.cookies,
48
+ store: STORE_ROUTE,
49
+ });
50
+ // The section whose list is shown (null: the first level). It follows the route's section, and the drawer opening.
51
+ const [shown, setShown] = useState<string | null>(place.section);
52
+ const [focus, setFocus] = useState<'list' | 'top' | undefined>();
53
+ const shownRef = useRef(shown);
54
+ shownRef.current = shown;
55
+ // The section a click in the rail asked for: the drawer opens at its list instead of at the route's section.
56
+ const asked = useRef<string | null>(null);
57
+ useEffect(() => {
58
+ setShown(asked.current ?? place.section);
59
+ setFocus(asked.current ? 'list' : undefined);
60
+ asked.current = null;
61
+ }, [place.section, open]);
62
+ const openAt = (section: string) => {
63
+ asked.current = section;
64
+ setOpen(true);
65
+ };
66
+ const show = (section: string | null) => {
67
+ setFocus(section === null ? 'top' : 'list');
68
+ setShown(section);
69
+ };
70
+ // Escape in a section's list goes to the first level; at the first level it is not used, so the modal drawer closes.
71
+ const onEscape = useCallback(() => {
72
+ if (shownRef.current === null) return false;
73
+ setFocus('top');
74
+ setShown(null);
75
+ return true;
76
+ }, []);
77
+ // Every level stays in the page (the screen check finds the screens from the links); the ones not shown are `hidden`.
78
+ const sections = tree.filter((entry) => entry.href === undefined);
79
+ return (
80
+ <MainDrawer
81
+ label={t.library.navigation}
82
+ open={open}
83
+ onOpenChange={setOpen}
84
+ onEscape={onEscape}
85
+ rail={tree.map((entry) =>
86
+ entry.href !== undefined ? (
87
+ <MainRailItem key={entry.id} href={entry.href} icon={icons[entry.id]} label={entry.label} selected={route === ''} />
88
+ ) : (
89
+ <MainRailSectionItem
90
+ key={entry.id}
91
+ icon={icons[entry.id]}
92
+ label={entry.label}
93
+ selected={place.section === entry.id}
94
+ onClick={() => openAt(entry.id)}
95
+ />
96
+ ),
97
+ )}
98
+ >
99
+ <MainDrawerLevel shown={shown === null} focus={focus}>
100
+ {tree.map((entry) =>
101
+ entry.href !== undefined ? (
102
+ <MainDrawerItem key={entry.id} href={entry.href} icon={icons[entry.id]} label={entry.label} selected={route === ''} />
103
+ ) : (
104
+ <MainDrawerSectionItem
105
+ key={entry.id}
106
+ icon={icons[entry.id]}
107
+ label={entry.label}
108
+ selected={place.section === entry.id}
109
+ onClick={() => show(entry.id)}
110
+ />
111
+ ),
112
+ )}
113
+ </MainDrawerLevel>
114
+ {sections.map((section) => (
115
+ <MainDrawerLevel key={section.id} shown={shown === section.id} focus={focus}>
116
+ <MainDrawerBack label={t.library.mainMenu} onClick={() => show(null)} />
117
+ {section.items.map(({ id, label, href, count, children }) => {
118
+ const here = place.section === section.id && place.item === id;
119
+ const shownCount = files ? count : null;
120
+ // An app's row has the app's icon (an installed app is a row of Apps, lib/menu.mjs).
121
+ const icon = (section.id === 'apps' && apps.find((a) => a.id === id)?.icon) || icons[id];
122
+ return children ? (
123
+ <MainDrawerFold key={id} href={href} icon={icon} label={label} count={shownCount} selected={here && !place.child} open={here}>
124
+ {children.map((child) => (
125
+ <MainDrawerSubitem key={child.id} href={child.href} label={child.label} selected={here && place.child === child.id} />
126
+ ))}
127
+ </MainDrawerFold>
128
+ ) : (
129
+ <MainDrawerItem key={id} href={href} icon={icon} label={label} count={shownCount} selected={here} />
130
+ );
131
+ })}
132
+ </MainDrawerLevel>
133
+ ))}
134
+ </MainDrawer>
135
+ );
136
+ }