@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
@@ -26,7 +26,7 @@ const { page, heading, headingHtml, sections } = Astro.props;
26
26
  shareTitle={page.data.shareTitle}
27
27
  shareDescription={page.data.shareDescription}
28
28
  >
29
- <Container class="pt-8 md:pt-16">
29
+ <Container class="pt-8 medium:pt-16">
30
30
  <h1 class="theme-title max-w-[22ch]">{headingHtml ? <Fragment set:html={headingHtml} /> : heading}</h1>
31
31
  </Container>
32
32
  {sections.map((s, i) => {
@@ -25,8 +25,8 @@ const posts = (await getPosts({ unlisted: true })).filter((p) => stateOf(p.data)
25
25
  <PageHeader title={siteT.theme.newPages} intro={siteT.theme.newPagesIntro} />
26
26
 
27
27
  {rows.length > 0 && (
28
- <Container as="section" class="mt-12 md:mt-16">
29
- <ul class="rounded-panel border border-line px-5 md:px-7">
28
+ <Container as="section" class="mt-12 medium:mt-16">
29
+ <ul class="rounded-panel border border-line px-5 medium:px-7">
30
30
  {rows.map(({ page, current }, i) => (
31
31
  <li id={previewId(page.data.path)} class:list={['scroll-mt-24 py-5', i > 0 && 'border-t border-line']}>
32
32
  <p class="type-title-medium">{pageHeading(page)}</p>
@@ -50,10 +50,10 @@ const posts = (await getPosts({ unlisted: true })).filter((p) => stateOf(p.data)
50
50
  )}
51
51
 
52
52
  {posts.length > 0 && (
53
- <Container as="section" class="mt-12 mb-20 md:mt-16 md:mb-28">
54
- <h2 class="type-headline-small md:type-headline-medium">{siteT.theme.newPosts}</h2>
53
+ <Container as="section" class="mt-12 mb-20 medium:mt-16 medium:mb-28">
54
+ <h2 class="type-headline-small medium:type-headline-medium">{siteT.theme.newPosts}</h2>
55
55
  <p class="mt-2 max-w-[60ch] text-muted">{siteT.theme.newPostsIntro}</p>
56
- <ul class="mt-5 rounded-panel border border-line px-5 md:px-7">
56
+ <ul class="mt-5 rounded-panel border border-line px-5 medium:px-7">
57
57
  {posts.map((p, i) => (
58
58
  <li class:list={['py-4', i > 0 && 'border-t border-line']}>
59
59
  <a href={postPath(p)} class="inline-flex items-center gap-1.5 font-medium hover:text-accent">
@@ -29,13 +29,13 @@ const index: [string, string][] = [
29
29
  ];
30
30
 
31
31
  // Less space above the first section than on the other pages: the suggestion sits between.
32
- const spaceOf = (i: number) => (i === 0 ? 'mt-6 md:mt-8' : space(i));
32
+ const spaceOf = (i: number) => (i === 0 ? 'mt-6 medium:mt-8' : space(i));
33
33
  ---
34
34
 
35
35
  <Base title={siteT.theme.notFound} noindex>
36
36
  <PageHeader title={heading} intro={intro} />
37
37
 
38
- <Container as="div" class="mt-8 md:mt-12">
38
+ <Container as="div" class="mt-8 medium:mt-12">
39
39
  <Card href="" id="suggestion" hidden className="group flex max-w-2xl items-center justify-between gap-6">
40
40
  <span>
41
41
  <span class="type-body-medium block text-muted">{siteT.theme.didYouMean}</span>
@@ -9,23 +9,57 @@
9
9
  // headline. It rests in one of its states (`state`, where it starts: 'collapsed', only the handle and the header;
10
10
  // 'half-expanded', at most half the screen's height; 'expanded', as high as what it shows, nearly to the top; or
11
11
  // 'hidden'), and its handle is a button (`dragHandleLabel`, what it says to a screen reader) that is dragged between
12
- // them or tapped to expand and collapse it; with a close button it can be hidden. On the site bottomSheet.script.ts
12
+ // them or tapped to step through them (up one at a time, from the highest back to the lowest); with a close button
13
+ // it can be hidden. `states` (data-states, space-separated) lists the states it may rest in; without it Material 3's
14
+ // (defaultStates in lib/sheet-stops.mjs, 'hidden' first when it has a close button). On the site bottomSheet.script.ts
13
15
  // lets it be dragged (standardBottomSheet() in bottomSheetControl.ts, which a block's or an app's script can also use); in
14
16
  // the editor it is drawn in its state and dragged here, at the bottom of its frame (which holds it with
15
17
  // contain: layout, and is a size container).
18
+ // `wide` is what it becomes from the medium breakpoint (data-wide): a standard sheet's 'side-sheet' (the default: drawn as a
19
+ // standard side sheet at the end of the screen, full height, without handle and buttons, every part shown, and
20
+ // standardBottomSheet() does nothing while the window is that wide) or 'sheet' (stays a bottom sheet, with margins,
21
+ // --bottom-sheet-margin-wide); a modal one's 'dialog' (the default), 'side-sheet' or 'sheet' (TypeScript rejects
22
+ // 'dialog' on a standard sheet). For a modal one 'side-sheet' is a modal side sheet at the end of the screen (full height,
23
+ // its scrim and tokens, without the handle) that bottomSheet() slides in from the end, and 'sheet' stays a bottom sheet
24
+ // with the margins and slides up; a container without the attribute (hand-written) is a 'dialog'.
25
+ // `startAt` (a modal sheet only, data-start; 'half' without it, also for a hand-written container without the attribute,
26
+ // Calendly's) is how high it opens on a phone: 'half', half the screen's height or as high as what is in it if that is
27
+ // less, or 'full', as high as the screen allows below --bottom-sheet-top-gap. It is pulled up to full height and down to
28
+ // close (modalSheetSize.ts); the content scrolls only at full height. The editor's sheets are not touched.
29
+ // `peek` (a standard sheet only) is what it shows even while collapsed (Material 3's preview), between the header and
30
+ // what it shows (.bottom-sheet-peek). A standard sheet is named by its headline or, without one, by `label` (the props are a
31
+ // union: TypeScript rejects a sheet with neither `headline` nor `label`; a modal one's <dialog> is named by the
32
+ // developer who writes it). The drag handle can be left out (`dragHandle={false}`: no handle, so no tap to step
33
+ // through the states; the header, if there is one, still drags it). The header is drawn only when there is a
34
+ // headline or a close button, or a resize or collapse button. `resizeLabel` adds the resize button (Material 3's
35
+ // alternative to dragging the handle for those who use a single pointer: it does what a tap on the handle does, its
36
+ // icon an up caret at the lowest stop and a down caret otherwise, `aria-expanded` saying which); a sheet without a
37
+ // handle that can be resized must have it (the props are a union again). `collapseLabel` adds the collapse button, which
38
+ // shows only while the sheet is as high as the screen allows (data-full, set by the control) and returns it to its
39
+ // lowest stop. `state` defaults to the lowest of `states` that is not 'hidden' ('collapsed' without
40
+ // `states`), so a fixed sheet (`states={['expanded']}`) starts in its only state.
16
41
  // The editor's own sheets (choices, fields) are modal ones in a <dialog> with bottomSheetFitClass, as high as what is in
17
42
  // them (bottomSheetFullClass: as high as the screen, a whole page in it), with the container and its header
18
43
  // (BottomSheetHeader, whose handle is a button there, `onHandle`, named `handleLabel`, and whose close button does
19
44
  // `onClose`).
20
45
  // How it looks is the theme's (--bottom-sheet-*).
21
- import { useEffect, useRef, type ReactNode } from 'react';
46
+ import { useEffect, useId, useRef, type ReactNode } from 'react';
22
47
  import { useOnSite } from '@morphemecms/cms/src/standard/view';
23
48
  import IconButton from '@theme/ui/IconButton';
49
+ import BottomSheetResizeIcon from '@theme/ui/BottomSheetResizeIcon';
24
50
  import { standardBottomSheet } from '@theme/ui/bottomSheetControl';
25
51
 
26
52
  /** The kinds of a bottom sheet, and the states a standard one rests in, as Material 3 names them. */
27
53
  export type BottomSheetKind = 'modal' | 'standard';
28
54
  export type BottomSheetState = 'collapsed' | 'half-expanded' | 'expanded' | 'hidden';
55
+ /** What a sheet becomes from the medium breakpoint; a standard one cannot be a dialog (the props are typed per kind). */
56
+ export type BottomSheetWide = 'side-sheet' | 'dialog' | 'sheet';
57
+ /** Where a modal sheet opens on a phone: at half the screen's height (or as high as what is in it, if that is less), or as
58
+ * high as the screen allows; a standard sheet has its `state` instead (the props are typed per kind). */
59
+ export type BottomSheetStart = 'half' | 'full';
60
+ export type BottomSheetKindWide =
61
+ | { kind?: 'modal'; wide?: BottomSheetWide; startAt?: BottomSheetStart }
62
+ | { kind: 'standard'; wide?: Exclude<BottomSheetWide, 'dialog'>; startAt?: never };
29
63
 
30
64
  /** The class of the <dialog> a modal bottom sheet is in, of what stands in place of what it shows (over a frame while
31
65
  * it loads, or instead of it), and a bottom sheet's container's classes. */
@@ -38,6 +72,26 @@ export const bottomSheetContainerClass = (kind: BottomSheetKind = 'modal') =>
38
72
  export const bottomSheetFitClass = 'bottom-sheet bottom-sheet-fit';
39
73
  export const bottomSheetFullClass = 'bottom-sheet bottom-sheet-full';
40
74
 
75
+ /** Whether a sheet's header is drawn: it has a headline or a button. */
76
+ const hasHeader = (headline: ReactNode, ...labels: (string | undefined)[]) => headline !== undefined || labels.some(Boolean);
77
+
78
+ /** What names a bottom sheet: a headline, or (without one) a `label`. */
79
+ export type BottomSheetName =
80
+ | { headline: Exclude<ReactNode, undefined | null>; headlineId?: string; label?: never }
81
+ | { label: string; headline?: never; headlineId?: never };
82
+
83
+ /** What a sheet without a drag handle needs to be resizable: a resize button, or states that leave nothing to resize
84
+ * (one other than 'hidden', evt. with 'hidden'). Material 3's states, which omitted `states` means, are resizable. */
85
+ type FixedStates = [Exclude<BottomSheetState, 'hidden'>] | ['hidden', Exclude<BottomSheetState, 'hidden'>];
86
+ export type BottomSheetResize =
87
+ | { dragHandle?: true; resizeLabel?: string }
88
+ | { dragHandle: false; resizeLabel: string }
89
+ | { dragHandle: false; resizeLabel?: undefined; states: FixedStates };
90
+
91
+ /** The state a standard sheet starts in when `state` is not given: the lowest of `states` that is not 'hidden'. */
92
+ const startState = (states?: BottomSheetState[]): BottomSheetState =>
93
+ (['collapsed', 'half-expanded', 'expanded'] as const).find((s) => !states || states.includes(s)) ?? 'collapsed';
94
+
41
95
  /** A bottom sheet's drag handle and header: its headline, the supporting text under it and evt. the close button. The
42
96
  * handle is a button with `handleLabel` (a standard sheet's, `expanded` saying whether it is up; the editor's, which
43
97
  * does `onHandle`); the close button does `onClose` in the editor. */
@@ -46,25 +100,33 @@ export function BottomSheetHeader({
46
100
  headlineId,
47
101
  supportingText,
48
102
  closeLabel,
103
+ resizeLabel,
104
+ collapseLabel,
49
105
  handleLabel,
50
106
  expanded,
107
+ raised,
51
108
  standard,
109
+ dragHandle = true,
52
110
  onHandle,
53
111
  onClose,
54
112
  }: {
55
- headline: ReactNode;
113
+ headline?: ReactNode;
56
114
  headlineId?: string;
57
115
  supportingText?: ReactNode;
58
116
  closeLabel?: string;
117
+ resizeLabel?: string;
118
+ collapseLabel?: string;
59
119
  handleLabel?: string;
60
120
  expanded?: boolean;
121
+ raised?: boolean;
61
122
  standard?: boolean;
123
+ dragHandle?: boolean;
62
124
  onHandle?: () => void;
63
125
  onClose?: () => void;
64
126
  }) {
65
127
  return (
66
128
  <>
67
- {handleLabel !== undefined ? (
129
+ {!dragHandle ? null : handleLabel !== undefined ? (
68
130
  <button
69
131
  type="button"
70
132
  className="bottom-sheet-drag-handle"
@@ -76,44 +138,67 @@ export function BottomSheetHeader({
76
138
  ) : (
77
139
  <div className="bottom-sheet-drag-handle" data-bottom-sheet-handle=""></div>
78
140
  )}
79
- <div className="bottom-sheet-header" data-bottom-sheet-header={standard ? '' : undefined}>
80
- <div className="bottom-sheet-heading">
81
- <h2 id={headlineId} className="bottom-sheet-headline">
82
- {headline}
83
- </h2>
84
- {supportingText && <p className="bottom-sheet-supporting-text">{supportingText}</p>}
141
+ {hasHeader(headline, closeLabel, resizeLabel, collapseLabel) && (
142
+ <div className="bottom-sheet-header" data-bottom-sheet-header={standard ? '' : undefined}>
143
+ <div className="bottom-sheet-heading">
144
+ {headline !== undefined && (
145
+ <h2 id={headlineId} className="bottom-sheet-headline">
146
+ {headline}
147
+ </h2>
148
+ )}
149
+ {supportingText && <p className="bottom-sheet-supporting-text">{supportingText}</p>}
150
+ </div>
151
+ {(resizeLabel || collapseLabel || closeLabel) && (
152
+ <div className="bottom-sheet-actions">
153
+ {resizeLabel && (
154
+ <IconButton icon={<BottomSheetResizeIcon />} label={resizeLabel} aria-expanded={!!raised} data-bottom-sheet-resize="" />
155
+ )}
156
+ {collapseLabel && <IconButton icon="caret-down" label={collapseLabel} data-bottom-sheet-collapse="" />}
157
+ {closeLabel && <IconButton icon="x" label={closeLabel} data-bottom-sheet-close="" onClick={onClose} />}
158
+ </div>
159
+ )}
85
160
  </div>
86
- {closeLabel && <IconButton icon="x" label={closeLabel} data-bottom-sheet-close="" onClick={onClose} />}
87
- </div>
161
+ )}
88
162
  </>
89
163
  );
90
164
  }
91
165
 
92
166
  export default function BottomSheet({
93
167
  kind = 'modal',
94
- state = 'collapsed',
168
+ wide,
169
+ startAt,
170
+ state,
171
+ states,
95
172
  headline,
96
173
  headlineId,
174
+ label,
97
175
  supportingText,
98
176
  closeLabel,
177
+ resizeLabel,
178
+ collapseLabel,
179
+ dragHandle = true,
99
180
  dragHandleLabel,
181
+ peek,
100
182
  footer,
101
183
  height,
102
184
  className,
103
185
  children,
104
186
  }: {
105
- kind?: BottomSheetKind;
106
187
  state?: BottomSheetState;
107
- headline: ReactNode;
108
- headlineId?: string;
188
+ states?: [BottomSheetState, ...BottomSheetState[]];
109
189
  supportingText?: ReactNode;
110
190
  closeLabel?: string;
191
+ collapseLabel?: string;
111
192
  dragHandleLabel?: string;
193
+ peek?: ReactNode;
112
194
  footer?: ReactNode;
113
195
  height?: string;
114
196
  className?: string;
115
197
  children: ReactNode;
116
- }) {
198
+ } & BottomSheetKindWide &
199
+ BottomSheetName &
200
+ BottomSheetResize) {
201
+ const shownState = state ?? startState(states);
117
202
  const standard = kind === 'standard';
118
203
  const onSite = useOnSite();
119
204
  const sheet = useRef<HTMLDivElement>(null);
@@ -121,26 +206,38 @@ export default function BottomSheet({
121
206
  useEffect(() => {
122
207
  if (standard && !onSite && sheet.current) standardBottomSheet(sheet.current);
123
208
  }, [standard, onSite]);
209
+ // A standard sheet is named by its headline even when the caller gives it no id.
210
+ const generatedId = useId();
211
+ const titleId = headlineId ?? generatedId;
124
212
  const Container = standard ? 'section' : 'div';
125
213
  return (
126
214
  <Container
127
215
  ref={sheet}
128
216
  className={[bottomSheetContainerClass(kind), className].filter(Boolean).join(' ')}
129
- aria-labelledby={standard ? headlineId : undefined}
217
+ aria-labelledby={standard && headline !== undefined ? titleId : undefined}
218
+ aria-label={standard && headline === undefined ? label : undefined}
130
219
  data-bottom-sheet-container=""
131
220
  data-bottom-sheet-standard={standard ? '' : undefined}
132
- data-state={standard ? state : undefined}
221
+ data-wide={wide ?? (standard ? 'side-sheet' : 'dialog')}
222
+ data-start={standard ? undefined : (startAt ?? 'half')}
223
+ data-state={standard ? shownState : undefined}
224
+ data-states={standard && states ? states.join(' ') : undefined}
133
225
  style={height ? { height } : undefined}
134
226
  >
135
227
  <BottomSheetHeader
136
228
  headline={headline}
137
- headlineId={headlineId}
229
+ headlineId={standard && headline !== undefined ? titleId : headlineId}
138
230
  supportingText={supportingText}
139
231
  closeLabel={closeLabel}
232
+ resizeLabel={standard ? resizeLabel : undefined}
233
+ collapseLabel={standard ? collapseLabel : undefined}
234
+ dragHandle={dragHandle}
140
235
  handleLabel={standard ? (dragHandleLabel ?? '') : undefined}
141
- expanded={state === 'half-expanded' || state === 'expanded'}
236
+ expanded={shownState === 'half-expanded' || shownState === 'expanded'}
237
+ raised={shownState !== 'hidden' && shownState !== startState(states)}
142
238
  standard={standard}
143
239
  />
240
+ {standard && peek && <div className="bottom-sheet-peek">{peek}</div>}
144
241
  <div className="bottom-sheet-content">{children}</div>
145
242
  {footer && <p className="bottom-sheet-footer">{footer}</p>}
146
243
  </Container>
@@ -0,0 +1,13 @@
1
+ // The icon of a bottom sheet's resize button (BottomSheet.tsx): the up caret while the sheet is at its lowest stop, the
2
+ // down caret otherwise; both are drawn and ui.css shows the one the button's aria-expanded says, so the control
3
+ // (bottomSheetControl.ts) only changes that attribute. Exposes BottomSheetResizeIcon, an IconRef element for IconButton.
4
+ import { Svg } from '@morphemecms/cms/src/icon';
5
+
6
+ export default function BottomSheetResizeIcon({ size = 22 }: { size?: number; className?: string }) {
7
+ return (
8
+ <>
9
+ <Svg name="caret-up" size={size} className="bottom-sheet-resize-up" />
10
+ <Svg name="caret-down" size={size} className="bottom-sheet-resize-down" />
11
+ </>
12
+ );
13
+ }
@@ -22,9 +22,10 @@ export type ButtonShape = (typeof buttonShapes)[number];
22
22
  export const buttonClass = (kind: ButtonKind = 'filled', size: ButtonSize = 'medium', shape: ButtonShape = 'round') =>
23
23
  ['button', `button-${kind}`, size !== 'medium' && `button-${size}`, shape !== 'round' && `button-${shape}`].filter(Boolean).join(' ');
24
24
 
25
- /** A toggle's icon (a Button's, an IconButton's): the icon, and the same icon filled while it is on, where the site
26
- * draws it filled (the theme shows the one of the state it is in: .<part>-unselected-icon, .<part>-selected-icon). */
27
- export function ToggleIcon({ part, name, size }: { part: 'button' | 'icon-button'; name: string; size: number }) {
25
+ /** A toggle's icon (a Button's, an IconButton's, a navigation drawer item's): the icon, and the same icon filled while
26
+ * it is on, where the site draws it filled (the theme shows the one of the state it is in: .<part>-unselected-icon,
27
+ * .<part>-selected-icon). */
28
+ export function ToggleIcon({ part, name, size }: { part: 'button' | 'icon-button' | 'navigation-drawer'; name: string; size: number }) {
28
29
  if (!iconFile(name, 'fill')) return <Svg name={name} size={size} />;
29
30
  return (
30
31
  <>
@@ -7,7 +7,7 @@
7
7
  // evt. a divider over the actions (`divider`) and the actions at its end (`actions`: buttons, the main one last).
8
8
  // - 'full-screen': over the whole screen on a phone, for a task with several steps; in its header the close button
9
9
  // (`closeLabel`, data-dialog-close), the headline and the action (`actions`: a text button), evt. a divider under it
10
- // (`divider`), and under them what it shows (`supportingText` and `children`, scrolling). From 48rem, where Material
10
+ // (`divider`), and under them what it shows (`supportingText` and `children`, scrolling). From the medium breakpoint, where Material
11
11
  // shows it as a dialog, it is in the middle of the screen over the scrim, with the same header.
12
12
  // Shown modally it is in a <dialog> with dialogModalClass (a full-screen one with dialogFullScreenModalClass), which
13
13
  // dialog() in dialogControl.ts (or the script that shows it: Popup.astro) opens and closes. How it looks is the theme's
@@ -1,17 +1,18 @@
1
1
  // A FAB menu (m3.material.io/components/fab-menu, .fab-menu in ../styles/ui.css): the theme's FAB (its classes,
2
- // fabClass: `icon`, its name `label`, its size `size`, any of the FAB sizes, and its colour `fabKind`, any of the FAB
3
- // colours, the menu's `kind` filled if it has none) that opens two to six related actions above it (`items`, each the
4
- // theme's ExtendedFab: its icon and label, with `href` a link), the last nearest the FAB. The menu comes in Material's
5
- // three colour sets (`kind`: primary, the default, secondary or tertiary): while it is open (aria-expanded) the FAB is a
6
- // round close button with a cross (its name `closeLabel`) in the set's filled colour, and the actions are in its
7
- // container colour, so they stand apart from it. Where it stands (fixed at the bottom of the screen) is the component's
8
- // (`className`). `open` draws it open (the editor). On the site ui/fabMenu.script.ts opens and closes it. Other
9
- // attributes (data-…) go on the whole.
2
+ // fabClass: `icon`, a name or an icon, its name `label`, its size `size`, any of the FAB sizes, and its colour
3
+ // `fabKind`, any of the FAB colours, the menu's `kind` filled if it has none) that opens two to six related actions
4
+ // above it (`items`, each the theme's ExtendedFab: its icon and label, with `href` a link), the last nearest the FAB.
5
+ // The menu comes in Material's three colour sets (`kind`: primary, the default, secondary or tertiary): while it is
6
+ // open (aria-expanded) the FAB is a round close button with a cross (its name `closeLabel`) in the set's filled
7
+ // colour, and the actions are in its container colour, so they stand apart from it. Where it stands (fixed at the
8
+ // bottom of the screen) is the component's (`className`). `open` draws it open (the editor). ui/fabMenu.script.ts
9
+ // opens and closes it (on the site, and in the editor through setUpFabMenu). Other attributes (data-…) go on the
10
+ // whole.
10
11
  import { useId } from 'react';
11
- import { Svg } from '@morphemecms/cms/src/icon';
12
+ import { Glyph, Svg, type IconRef } from '@morphemecms/cms/src/icon';
12
13
  import { ExtendedFab, fabClass, type FabKind, type FabSize } from '@theme/ui/Fab';
13
14
 
14
- export type FabMenuItem = { icon: string; label: string; href?: string } & Record<string, unknown>;
15
+ export type FabMenuItem = { icon: IconRef; label: string; href?: string } & Record<string, unknown>;
15
16
  export const fabMenuKinds = ['primary', 'secondary', 'tertiary'] as const;
16
17
  export type FabMenuKind = (typeof fabMenuKinds)[number];
17
18
 
@@ -30,7 +31,7 @@ export default function FabMenu({
30
31
  className,
31
32
  ...rest
32
33
  }: {
33
- icon: string;
34
+ icon: IconRef;
34
35
  label: string;
35
36
  closeLabel: string;
36
37
  items: FabMenuItem[];
@@ -52,7 +53,7 @@ export default function FabMenu({
52
53
  data-label={label}
53
54
  data-close-label={closeLabel}
54
55
  >
55
- <Svg name={icon} size={24} className="fab-menu-icon" />
56
+ <Glyph icon={icon} size={24} className="fab-menu-icon" />
56
57
  <Svg name="x" size={24} className="fab-menu-close-icon" />
57
58
  </button>
58
59
  <ul id={list} className="fab-menu-list" aria-label={label} hidden={!open}>
@@ -2,7 +2,7 @@
2
2
  // ../styles/ui.css): a shape that turns and changes from one of Material's shapes into the next, while something that
3
3
  // takes only a moment is on its way (where how far it has come is known, a progress indicator says it instead). By
4
4
  // itself, or in its round container (`contained`, where it stands on something else). Screen readers read it as a
5
- // progress bar without a value, named by `label` (e.g. "Henter …"). With reduced motion it only turns, slowly. How it
5
+ // progress bar without a value, named by `label` (e.g. "Henter…"). With reduced motion it only turns, slowly. How it
6
6
  // looks is the theme's (--loading-indicator-*). An Astro component gives an element the same classes with
7
7
  // loadingIndicatorClass, with a .loading-indicator-shape in it.
8
8
 
@@ -1,48 +1,149 @@
1
- // A modal navigation drawer (m3.material.io/components/navigation-drawer, .navigation-drawer in ../styles/ui.css): the
2
- // site's pages in a panel over the page (components/MobileMenu.tsx, and a theme's own menu), over its scrim
3
- // (navigationDrawerScrimClass). Its pages are items (NavigationDrawerItem, `active`: the open page or the one holding
4
- // it), an item with sub-pages is a row (NavigationDrawerRow) with the button that unfolds them after it
1
+ // A navigation drawer (m3.material.io/components/navigation-drawer, .navigation-drawer in ../styles/ui.css), at the
2
+ // start edge: the site's pages in a panel over the page (components/MobileMenu.tsx, and a theme's own menu), over its
3
+ // scrim (navigationDrawerScrimClass). Its pages are items (NavigationDrawerItem, `active`: the open page or the one
4
+ // holding it), an item with sub-pages is a row (NavigationDrawerRow) with the button that unfolds them after it
5
5
  // (NavigationDrawerToggle, `expanded`), and a sub-page a subitem (NavigationDrawerSubitem). Where it comes from, how it
6
6
  // moves and what else is in it (the logo, the buttons) is the menu's own: it gives its own element (a motion.div) the
7
7
  // class with navigationDrawerClass; in a <dialog> (the editor's), the dialog has navigationDrawerDialogClass and its
8
- // scrim is the dialog's. Links are Link (on the site; in the editor's previews only drawn), an item without `href` a
9
- // button (the editor's: a part of a form); other attributes (onClick, aria-…) go on them.
10
- import type { ReactNode } from 'react';
11
- import { Svg } from '@morphemecms/cms/src/icon';
8
+ // scrim is the dialog's. Its variant (variant): modal, the default, over its scrim, or standard, beside the content at
9
+ // full height without a scrim and without a shadow (navigationDrawerClasses gives the classes to an element of its own);
10
+ // where it stands is the component's own. Links are Link (on the site; in the editor's previews only drawn), an item
11
+ // without `href` a button (the editor's: a part of a form); other attributes (onClick, aria-…) go on them. An item has
12
+ // an icon before its label (icon: a Phosphor name, filled while it is active, or an icon made) and evt. a badge after
13
+ // the label (badge: how many there are, label large). The pages can stand in sections (NavigationDrawerSection, label:
14
+ // its name, title small), a full-width line (the theme's Divider) between them. The arrow keys, Home and End move
15
+ // between the items (NavigationDrawer does it itself; a drawer drawn as a class on an element of its own calls
16
+ // navigationDrawerKeys(root)), and a modal one gives its first item the focus when it opens
17
+ // (focusNavigationDrawerItem(root); navigationDrawerControl.ts). An item can end in an icon (trailing: an arrow to a list
18
+ // it opens), and a drawer in two levels has a back row (NavigationDrawerBack, an arrow and a label) at the top of the
19
+ // second level; it is a button, so the arrow keys reach it like an item.
20
+ import type { KeyboardEvent, ReactNode } from 'react';
21
+ import { Glyph, Svg, type IconRef } from '@morphemecms/cms/src/icon';
22
+ import { ToggleIcon } from '@theme/ui/Button';
12
23
  import { Link } from '@morphemecms/cms/src/standard/view';
24
+ import Divider from '@theme/ui/Divider';
25
+ import { handleDrawerKey } from './navigationDrawerControl';
26
+ export { focusNavigationDrawerItem, navigationDrawerKeys } from './navigationDrawerControl';
27
+
28
+ export type NavigationDrawerVariant = 'modal' | 'standard';
13
29
 
14
30
  /** The drawer's class, and its scrim's. */
15
31
  export const navigationDrawerClass = 'navigation-drawer';
16
32
  export const navigationDrawerScrimClass = 'navigation-drawer-scrim';
17
- /** The class of a <dialog> the drawer is in: at the end of the screen, as high as it, over its scrim. */
33
+ /** The class of a <dialog> the drawer is in: at the start of the screen, as high as it, over its scrim. */
18
34
  export const navigationDrawerDialogClass = 'navigation-drawer-dialog';
19
35
 
20
- export function NavigationDrawer({ className, children, ...rest }: { className?: string; children: ReactNode } & Record<string, unknown>) {
36
+ /** The drawer's classes with its variant: `modal` (the default) over a scrim, `standard` beside the content, full height. */
37
+ export const navigationDrawerClasses = (variant: NavigationDrawerVariant = 'modal') =>
38
+ [navigationDrawerClass, variant === 'standard' && 'navigation-drawer-standard'].filter(Boolean).join(' ');
39
+
40
+ export function NavigationDrawer({
41
+ variant = 'modal',
42
+ className,
43
+ children,
44
+ onKeyDown,
45
+ ...rest
46
+ }: {
47
+ variant?: NavigationDrawerVariant;
48
+ className?: string;
49
+ children: ReactNode;
50
+ onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
51
+ } & Record<string, unknown>) {
52
+ return (
53
+ <div
54
+ className={[navigationDrawerClasses(variant), className].filter(Boolean).join(' ')}
55
+ onKeyDown={(event) => {
56
+ onKeyDown?.(event);
57
+ if (!event.defaultPrevented) handleDrawerKey(event.nativeEvent, event.currentTarget);
58
+ }}
59
+ {...rest}
60
+ >
61
+ {children}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ /** Items under each other in a section, with its name (`label`) over them. Between two sections stands a full-width
67
+ * divider (drawn by the section that has one before it, shown by the theme), never between single items. */
68
+ export function NavigationDrawerSection({ label, children }: { label?: string; children: ReactNode }) {
21
69
  return (
22
- <div className={[navigationDrawerClass, className].filter(Boolean).join(' ')} {...rest}>
70
+ <div className="navigation-drawer-section" role={label ? 'group' : undefined} aria-label={label}>
71
+ <Divider className="navigation-drawer-section-divider" />
72
+ {label && (
73
+ <span className="navigation-drawer-section-label" aria-hidden="true">
74
+ {label}
75
+ </span>
76
+ )}
23
77
  {children}
24
78
  </div>
25
79
  );
26
80
  }
27
81
 
28
- /** A page in the drawer, `active` when it is the open page or holds it; without `href` a button. */
82
+ /** The icon before an item's label: a Phosphor name is drawn outlined, and filled on the active item where the site draws
83
+ * a filled one (ToggleIcon); an icon made is drawn as it is. */
84
+ function ItemIcon({ icon }: { icon: IconRef }) {
85
+ return (
86
+ <span className="navigation-drawer-icon">
87
+ {typeof icon === 'string' ? <ToggleIcon part="navigation-drawer" name={icon} size={24} /> : <Glyph icon={icon} size={24} />}
88
+ </span>
89
+ );
90
+ }
91
+
92
+ /** A page in the drawer, `active` when it is the open page or holds it; without `href` a button. With an `icon` (24dp,
93
+ * filled when it is active; Material: all items have one or none), evt. a badge (the text at its end: how many there
94
+ * are) and evt. a `trailing` icon at the very end (24dp, outlined; an arrow to the list the item opens). */
29
95
  export function NavigationDrawerItem({
30
96
  href,
31
97
  active,
98
+ icon,
99
+ badge,
100
+ trailing,
32
101
  children,
33
102
  ...rest
34
- }: { href?: string; active?: boolean; children: ReactNode } & Record<string, unknown>) {
103
+ }: {
104
+ href?: string;
105
+ active?: boolean;
106
+ icon?: IconRef;
107
+ badge?: ReactNode;
108
+ trailing?: IconRef;
109
+ children: ReactNode;
110
+ } & Record<string, unknown>) {
111
+ const content = (
112
+ <>
113
+ {icon !== undefined && <ItemIcon icon={icon} />}
114
+ <span className="navigation-drawer-label">{children}</span>
115
+ {badge != null && badge !== '' && <span className="navigation-drawer-badge">{badge}</span>}
116
+ {trailing !== undefined && (
117
+ <span className="navigation-drawer-trailing">
118
+ {typeof trailing === 'string' ? <Svg name={trailing} size={24} /> : <Glyph icon={trailing} size={24} />}
119
+ </span>
120
+ )}
121
+ </>
122
+ );
35
123
  return href === undefined ? (
36
124
  <button type="button" className="navigation-drawer-item" data-active={active || undefined} {...rest}>
37
- {children}
125
+ {content}
38
126
  </button>
39
127
  ) : (
40
128
  <Link href={href} className="navigation-drawer-item" data-active={active || undefined} {...rest}>
41
- {children}
129
+ {content}
42
130
  </Link>
43
131
  );
44
132
  }
45
133
 
134
+ /** The back row at the top of a drawer's second level: an `arrow-left` and the `label` (the first level's name). A
135
+ * button; `onClick` shows the first level. */
136
+ export function NavigationDrawerBack({ label, ...rest }: { label: string } & Record<string, unknown>) {
137
+ return (
138
+ <button type="button" className="navigation-drawer-item navigation-drawer-back" {...rest}>
139
+ <span className="navigation-drawer-icon">
140
+ <Svg name="arrow-left" size={24} />
141
+ </span>
142
+ <span className="navigation-drawer-label">{label}</span>
143
+ </button>
144
+ );
145
+ }
146
+
46
147
  /** An item and what stands after it (the toggle of its sub-pages). */
47
148
  export function NavigationDrawerRow({
48
149
  className,
@@ -15,6 +15,7 @@
15
15
  // editor's logo and the site's name). With `onExpandedChange` the rail is expanded and collapsed by whoever draws it (the
16
16
  // editor) instead of by its script; a modal one without `scrim` has none (one that expands while the mouse is over it:
17
17
  // the editor's).
18
+ // A destination without `href` is a button (the editor's sections, which open their list).
18
19
  // Where it stands (beside the content, in a row) is the component's own; how it looks is the theme's. Links are Link (on
19
20
  // the site; in the editor only drawn). An Astro component gives its elements the classes with navigationRailClass,
20
21
  // navigationRailClasses and navigationRailItemClass.
@@ -119,7 +120,7 @@ export function NavigationRailSection({ label, children }: { label: string; chil
119
120
  );
120
121
  }
121
122
 
122
- /** A destination: its icon (a Phosphor name, the theme's icon, or an icon made: IconRef) and its label, `selected` when
123
+ /** A destination, a link (or, without `href`, a button): its icon (a Phosphor name, the theme's icon, or an icon made: IconRef) and its label, `selected` when
123
124
  * it is the open page, evt. a badge on the icon, and a text at its end while the rail is expanded. */
124
125
  export function NavigationRailItem({
125
126
  href,
@@ -130,25 +131,33 @@ export function NavigationRailItem({
130
131
  trailingSupporting,
131
132
  ...rest
132
133
  }: {
133
- href: string;
134
+ href?: string;
134
135
  icon: IconRef;
135
136
  label: ReactNode;
136
137
  selected?: boolean;
137
138
  badge?: true | number;
138
139
  trailingSupporting?: ReactNode;
139
140
  } & Record<string, unknown>) {
140
- return (
141
- <Link href={href} className={navigationRailItemClass} aria-current={selected ? 'page' : undefined} {...rest}>
142
- <span className="navigation-rail-indicator">
143
- <span className="navigation-rail-icon">
144
- <Glyph icon={icon} size={24} />
145
- {badge !== undefined && <Badge count={badge === true ? undefined : badge} className="navigation-rail-badge" />}
146
- </span>
147
- <span className="navigation-rail-label">{label}</span>
148
- {trailingSupporting != null && trailingSupporting !== '' && (
149
- <span className="navigation-rail-trailing-supporting">{trailingSupporting}</span>
150
- )}
141
+ const content = (
142
+ <span className="navigation-rail-indicator">
143
+ <span className="navigation-rail-icon">
144
+ <Glyph icon={icon} size={24} />
145
+ {badge !== undefined && <Badge count={badge === true ? undefined : badge} className="navigation-rail-badge" />}
151
146
  </span>
147
+ <span className="navigation-rail-label">{label}</span>
148
+ {trailingSupporting != null && trailingSupporting !== '' && (
149
+ <span className="navigation-rail-trailing-supporting">{trailingSupporting}</span>
150
+ )}
151
+ </span>
152
+ );
153
+ // Without `href` a button (the editor's: a section that opens its list in the expanded rail); `selected` is aria-current.
154
+ return href === undefined ? (
155
+ <button type="button" className={navigationRailItemClass} aria-current={selected ? 'true' : undefined} {...rest}>
156
+ {content}
157
+ </button>
158
+ ) : (
159
+ <Link href={href} className={navigationRailItemClass} aria-current={selected ? 'page' : undefined} {...rest}>
160
+ {content}
152
161
  </Link>
153
162
  );
154
163
  }