@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
@@ -9,7 +9,7 @@ export const parts = {
9
9
  toolbarButton:
10
10
  'A tool in the text’s toolbar: a little smaller, so the bar fits on a phone. With an arrow it opens a menu (the kind of text, when the bar is too narrow).',
11
11
  fields:
12
- 'The theme’s fields with the name on their edge and an explanation below. A choice opens the sheet, or a page of its own with what each choice gives (the arrow points there).',
12
+ 'The theme’s fields with the name on their edge and an explanation below. A choice opens a menu right under the field on a computer, or the sheet on a phone, or a page of its own with what each choice gives (the arrow points there).',
13
13
  formField: 'A field that says what is wrong with what was typed, and the number of characters out of the most allowed.',
14
14
  fieldGroup: 'Fields that belong together, in a frame with their name on it.',
15
15
  shownValue: 'What a field holds, shown without being edited here: with its icon, or gray when it is made elsewhere.',
@@ -27,15 +27,17 @@ export const parts = {
27
27
  'The search with the buttons beside it, always in the same order: sort, filter, date, Google, and menu or list. Sort is gray while the pages are shown as the menu.',
28
28
  choiceTile: 'One of many icons to tap, in a grid.',
29
29
  group:
30
- 'Rows in a frame under a small heading: the start screen and the lists. The icon looks the same everywhere: gray, and in the accent color for the open part in the sidebar. A small picture or a sample of one of the theme’s tokens can stand before the name instead. A number may stand at the right. A row can also do something, in the accent color; delete, in the middle; or only be shown, with no color under the mouse and perhaps gray.',
30
+ 'Rows in a frame under a small heading: the start screen and the lists. The icon looks the same everywhere: gray, and in the accent color for the open part in the menu. A small picture or a sample of one of the theme’s values can stand before the name instead. A number may stand at the right. A row can also do something, in the accent color; delete, in the middle; or only be shown, with no color under the mouse and perhaps gray.',
31
31
  checkList:
32
32
  'Things that were checked: a score from 0 to 100 in a ring, a red triangle by what could be better (which folds out with how), and a green dot by what is fine.',
33
33
  scoreRing:
34
34
  'A score from 0 to 100 in a small ring at the start of a row, in the colors of the ring of checked things: green from 90, orange from 50, else red.',
35
+ menuRow:
36
+ 'A setting in the same row as a switch: its name and what it does at the left, and at the right the chosen one in a small box with an arrow. It opens the choices under it (a sheet on a phone), the chosen one checked. Gray while the setting cannot be changed.',
35
37
  unfold:
36
38
  'At the right of a row: how many rows are under it, and the arrow that shows them. The rows under it are indented along a line.',
37
39
  sheet:
38
- 'The sheet from the bottom (a dialog on a computer) with choices or fields. An icon or a row, for example, Language, opens it with the choices, and + (NewChoice) with the kinds something new can be, for example, a pop-up. The choices can stand under small headings.',
40
+ 'The sheet from the bottom (a dialog on a computer) with choices or fields. An icon or a row, for example, Language, opens it with the choices, and + opens it with the kinds something new can be, for example, a pop-up. The choices can stand under small headings.',
39
41
  preview:
40
42
  'The preview of a block in a dashed frame. It can be used as on the site, for example, folded out, but a link in it goes nowhere.',
41
43
  colorSwatches: 'A set of colors in small rounds side by side, for example, 4 of the website’s colors.',
@@ -50,9 +52,15 @@ export const parts = {
50
52
  'The sheet with a whole page in it (the preview), as tall as the screen. The page is in a frame, as wide as a phone or a computer, and nothing in it runs.',
51
53
  foldSection:
52
54
  'A section of a long form (a page, a blog post) that folds in and out by its heading. Only the first is open from the start.',
53
- sharePreview: "A link to a page as it looks when it is shared: the picture, the site's domain, the title, and the description.",
55
+ sharePreview: "A link to a page as it looks when it is shared: the picture, the website's domain, the title, and the description.",
54
56
  formColumns:
55
57
  'A form on a wide screen: the text at the left, the status and the other fields on cards at the right. On a smaller screen below each other. The heading over the fields is always the same.',
58
+ mainDrawer:
59
+ 'The editor’s menu: the website’s parts in sections, the open one marked. On a wide screen it stands beside the content, and the menu button at the start of the top bar shows and hides it; on a smaller screen it opens over the page and closes on a choice, a tap beside it, Escape or a swipe. Shown here opening over the page.',
60
+ editorFooter:
61
+ 'The line at the bottom of every screen, under the content, with a link to the licenses of what the website and the editor are made with.',
62
+ sectionList:
63
+ 'The sections of a long form as a list, with the one in view selected. On a large screen it is in a pane at the end of the form, shown and hidden by the button at the end of the top bar; on a smaller screen the button opens it in a side sheet, which closes when you choose a section.',
56
64
  },
57
65
  /** Over the parts: the search, and the groups to show one of (as Material 3 sorts its components). */
58
66
  filter: {
@@ -141,7 +149,7 @@ export const parts = {
141
149
  `The fields of the forms and settings, one of each type with the choices it can have. The fields are in ${file} and are built of the parts above. Nothing here is saved.`,
142
150
  use: {
143
151
  string:
144
- 'One line of text. It can have to be filled in, have at most a number of characters, have a text that is used when the field is empty, have to have a certain form, be an address, a number in pairs, a phone number, or words as chips (one or more), or have a link to where it is edited.',
152
+ 'One line of text. It can be required, have a length limit, show a text while it is empty, need a certain form, hold an address, a number in pairs, a phone number, or words as chips, or link to where it is edited.',
145
153
  text: 'Text over several lines, with the same choices as one line.',
146
154
  markdown: 'Text with formatting, in the same editor as the pages.',
147
155
  number: 'A number.',
@@ -150,7 +158,7 @@ export const parts = {
150
158
  select:
151
159
  'One of several choices: in the sheet from the bottom, side by side, as icons, or only shown. A choice can have its own explanation, and a field can be there only with one of the choices.',
152
160
  relation:
153
- 'A page, a post, or a tag from the site’s lists. A page is picked with the page picker, the fixed pages too; several picked are chips.',
161
+ 'A page, a post, or a tag from the website’s lists. A page is picked with the page picker, the fixed pages too; several picked are chips.',
154
162
  image: 'A picture from Files or a new one.',
155
163
  readonly: 'Shown, but not edited here.',
156
164
  object: 'A group of fields in a frame.',
@@ -164,7 +172,7 @@ export const parts = {
164
172
  heading: 'Heading',
165
173
  headingHint: 'At most about 60 characters.',
166
174
  underName: 'Text (under the name)',
167
- underNameHint: 'If the field is empty, the site’s name is used.',
175
+ underNameHint: 'If the field is empty, the website’s name is used.',
168
176
  email: 'Email',
169
177
  emailForm: 'Write a whole email address',
170
178
  cvr: 'CVR number',
@@ -182,9 +190,9 @@ export const parts = {
182
190
  shown: 'Shown',
183
191
  hidden: 'Hidden',
184
192
  listText: 'Text (in the list)',
185
- listTextHint: 'Only there with the placement In the list.',
186
- publishedHint: 'The page is on the site.',
187
- draftHint: 'The page isn’t on the site.',
193
+ listTextHint: 'Shown only with the placement In the list.',
194
+ publishedHint: 'The page is on the website.',
195
+ draftHint: 'The page isn’t on the website.',
188
196
  look: 'Look',
189
197
  kind: 'Kind',
190
198
  page: 'Page',
@@ -384,7 +392,7 @@ export const parts = {
384
392
  fieldSupporting: 'You only get an email when there’s news.',
385
393
  fieldError: 'Write an email address.',
386
394
  fieldPlaceholder: 'Your email',
387
- fieldSearch: 'Search the site',
395
+ fieldSearch: 'Search the website',
388
396
  fieldClear: 'Clear',
389
397
  fieldPhone: 'Phone',
390
398
  fieldPrefix: '+45',
@@ -401,7 +409,7 @@ export const parts = {
401
409
  fieldReadOnly: 'Customer number',
402
410
  fieldReadOnlyValue: '10 482',
403
411
  checkboxLabel: 'Statistics',
404
- checkboxSupporting: 'How the site is used.',
412
+ checkboxSupporting: 'How the website is used.',
405
413
  checkboxParent: 'All cookies',
406
414
  checkboxError: 'Check the box to continue.',
407
415
  /** The theme's chips (Chip), a group of their own under "UI components". */
@@ -427,10 +435,16 @@ export const parts = {
427
435
  'Cards or pictures in a row that are swiped sideways, with dots under them: all the same size, in several sizes, one large (from the start or in the middle) and one at a time.',
428
436
  dialogKinds: { basic: 'Basic', icon: 'With icon, list, and divider', container: 'Only the box', fullScreen: 'Full screen' },
429
437
  sheetKinds: {
430
- modal: 'Modal',
438
+ modal: 'Modal, a dialog on a computer',
439
+ modalSideSheet: 'Modal, a side sheet on a computer',
440
+ modalSheet: 'Modal, stays a sheet on a computer',
431
441
  collapsed: 'Standard, collapsed',
432
442
  halfExpanded: 'Standard, half expanded',
433
443
  expanded: 'Standard, expanded',
444
+ fixed: 'Standard, fixed, no handle or headline',
445
+ resizeButton: 'Standard, resize button instead of the handle',
446
+ collapseButton: 'Standard, collapse button at full height',
447
+ wideSheet: 'Standard, stays a sheet on a computer, while the others become a side sheet there',
434
448
  },
435
449
  layouts: {
436
450
  uncontained: 'All the same size',
@@ -446,11 +460,28 @@ export const parts = {
446
460
  save: 'Save',
447
461
  page: 'The page behind the sheet can still be used.',
448
462
  dragHandle: 'Expand or collapse the sheet',
463
+ openSheet: 'Open the modal sheet',
464
+ openSideSheet: 'Open the modal sheet as a side sheet',
465
+ openWideSheet: 'Open the modal sheet with margins',
466
+ paragraphs: [
467
+ 'On a phone the sheet opens at half the height of the screen.',
468
+ 'Pull the handle, the heading or this text up to make it as high as the screen allows.',
469
+ 'Once it is at full height, this text scrolls.',
470
+ 'Swipe down from the top of this text, or pull the handle down, to close it.',
471
+ 'A fast flick up or down goes straight to full height or closes it.',
472
+ 'A short pull down and let go returns it to half height.',
473
+ 'On a computer the sheet is a dialog, a side sheet or a sheet with margins, as its example says.',
474
+ 'This text only makes the sheet long enough to need scrolling.',
475
+ ],
449
476
  picture: (n: number) => `Picture ${n}`,
450
477
  showPicture: (n: number) => `Show picture ${n}`,
451
478
  heading: 'Heading',
452
479
  text: 'A short text about what the box is about.',
453
480
  sheetText: 'A short text under the heading',
481
+ sheetPeek: 'A line that shows while the sheet is collapsed',
482
+ sheetLabel: 'Details',
483
+ resize: 'Expand or collapse the sheet',
484
+ collapse: 'Collapse the sheet',
454
485
  footer: 'A line at the bottom',
455
486
  close: 'Close',
456
487
  roleDescription: 'carousel',
@@ -462,7 +493,7 @@ export const parts = {
462
493
  navigation: {
463
494
  title: 'Navigation',
464
495
  topAppBar:
465
- "The bar at the top of a page: only the surface with the site's own header, or the button at the start, the headline, and the buttons at the end, small, medium, or large, or with the search field. It gets another surface when the page scrolls under it.",
496
+ "The bar at the top of a page: only the surface with the website's own header, or the button at the start, the headline, and the buttons at the end, small, medium, or large, or with the search field. It gets another surface when the page scrolls under it.",
466
497
  stuck: 'When the page scrolls',
467
498
  /** The top app bar's kinds and what is in them (TopAppBar.tsx). */
468
499
  appBar: {
@@ -472,9 +503,9 @@ export const parts = {
472
503
  long: 'A long headline that may run over several lines',
473
504
  collapsed: 'collapsed',
474
505
  back: 'Back',
475
- search: 'Search the site',
506
+ search: 'Search the website',
476
507
  clear: 'Clear',
477
- empty: 'Nothing fits the search.',
508
+ empty: 'Nothing fits the search',
478
509
  favourite: 'Save',
479
510
  profile: 'Profile',
480
511
  },
@@ -502,7 +533,13 @@ export const parts = {
502
533
  error: 'Choose a treatment.',
503
534
  kinds: { outlined: 'Outlined', filled: 'Filled' },
504
535
  },
505
- navigationDrawer: 'The menu that opens over the page on a phone: the pages, one with sub-pages unfolded, the open one marked.',
536
+ navigationDrawer:
537
+ 'The menu that opens over the page on a phone: the pages, one with sub-pages unfolded, the open one marked. The standard one stands beside the page, with sections, icons and counts, and in two levels: sections with an arrow, and a list under a back row.',
538
+ /** The standard drawer's made-up sections and items (NavigationParts.tsx). */
539
+ drawerSections: ['Mail', 'Account'],
540
+ drawerItems: ['Inbox', 'Messages', 'Favorites', 'Trash', 'Profile'],
541
+ /** The back row of the drawer's second level (NavigationParts.tsx). */
542
+ drawerBack: ['Main menu'],
506
543
  tabs: 'Tabs in a row: only the text, as under the bar, or primary with an icon and secondary, with a line under the open one. To swipe sideways or across the whole width.',
507
544
  /** The tabs' made-up views (Tabs.tsx). */
508
545
  views: ['Overview', 'Prices', 'Reviews', 'Contact', 'Map', 'Videos'],
@@ -556,6 +593,8 @@ export const parts = {
556
593
  selection: {
557
594
  title: 'Switches, choices and side sheets',
558
595
  switch: 'Turns something on or off, with its name before it: off, on, with icons in the handle, and gray when it can’t be changed.',
596
+ settingRow:
597
+ 'A setting on one line: its name and what it does at the start, and what sets it at the end (a switch, a dropdown button).',
559
598
  radioButton: 'One of a few choices in a group under its name, maybe with a line under it: chosen and gray when it can’t be chosen.',
560
599
  slider:
561
600
  'Chooses a number by dragging: freely, in steps with dots, from the middle, a range with 2 handles and with the number over the handle, in 5 sizes, with an icon and vertical.',
@@ -605,12 +644,12 @@ export const parts = {
605
644
  alone: 'Alone',
606
645
  contained: 'In a container',
607
646
  loading: 'Loading…',
608
- uploading: 'Uploading',
647
+ uploading: 'Uploading…',
609
648
  percent: (v: number) => `${Math.round(v * 100)}%`,
610
649
  indeterminate: 'Without a value',
611
650
  shapes: { flat: 'Flat', wavy: 'Wavy' },
612
651
  thick: 'Thick',
613
- sent: 'The message has been sent.',
652
+ sent: 'The message has been sent',
614
653
  booked: 'Your time is booked. You’ll get a confirmation by email shortly.',
615
654
  undo: 'Undo',
616
655
  seeBooking: 'See the booking',
@@ -640,10 +679,10 @@ export const parts = {
640
679
  sections: ['Start', 'You'],
641
680
  menu: 'Change width',
642
681
  compose: 'Write',
643
- searchLabel: 'Search the site',
682
+ searchLabel: 'Search the website',
644
683
  back: 'Back',
645
684
  clear: 'Clear',
646
- empty: 'Nothing fits the search.',
685
+ empty: 'Nothing fits the search',
647
686
  query: 'cou',
648
687
  results: [
649
688
  { headline: 'Prices', supporting: 'Searched recently' },
@@ -4,12 +4,11 @@ export const preview = {
4
4
  /** The button, the icon in the top bar, and the sheet. */
5
5
  button: 'Preview',
6
6
  title: 'Preview',
7
- /** How wide the page is shown. */
8
- device: 'Show as',
9
- phone: 'Phone',
10
- computer: 'Computer',
11
- making: 'Making the preview…',
7
+ /** How wide the page is shown: Material 3's window sizes, each at its narrowest. */
8
+ size: 'Window size',
9
+ sizes: { compact: 'Compact', medium: 'Medium', expanded: 'Expanded', large: 'Large', extraLarge: 'Extra-large' },
10
+ making: 'Loading the preview…',
12
11
  failed: (error: string) => `Couldn’t create the preview: ${error}`,
13
12
  notHere: 'That page can’t be previewed',
14
- pageNotLoaded: (status: number) => `The page answered ${status}`,
13
+ pageNotLoaded: (status: number) => `The page couldn’t be loaded, error ${status}`,
15
14
  };
@@ -26,13 +26,13 @@ export const redirects = {
26
26
  pageFilter: 'Show by page',
27
27
  /** Over the posts among its choices. */
28
28
  postsGroup: 'Blog posts',
29
- loading: 'Loading the redirects',
29
+ loading: 'Loading the redirects…',
30
30
  /** None, or none matching the search. */
31
31
  none: (query: string) => `No redirects${query ? ` with "${query}"` : ''}`,
32
32
  from: 'From',
33
33
  fromHint: 'For example, /old-page/.',
34
34
  to: 'To',
35
- toHint: 'For example, /about-us/, or / for the home page.',
35
+ toHint: 'For example, /about/, or / for the home page.',
36
36
  /** Where it sends to; `noPage`: an address of the site no page in the editor has. */
37
37
  sendsTo: (name: string, noPage: boolean) => `Goes to ${name}${noPage ? ', which isn’t a page in the editor' : ''}.`,
38
38
  deleteRedirect: 'Delete redirect',
@@ -42,7 +42,7 @@ export const redirects = {
42
42
  title: 'Not found',
43
43
  intro: 'Addresses visitors tried to open that don’t exist.',
44
44
  searchLabel: 'Search the addresses',
45
- loading: 'Loading the addresses',
45
+ loading: 'Loading the addresses…',
46
46
  notLoaded: (error: string) => `Couldn’t load the addresses that don’t exist: ${error}`,
47
47
  /** The site does not keep them (yet). */
48
48
  notKept: 'The website doesn’t yet record addresses that don’t exist',
@@ -10,7 +10,7 @@ export const seo = {
10
10
  sorts: { low: 'Lowest first', high: 'Highest first', az: 'A–Z', za: 'Z–A' },
11
11
  /** The ring, read aloud. */
12
12
  scoreLabel: (score: number) => `SEO rating ${score} of 100`,
13
- /** The filters in the funnel: by the colour of the rating, by whether a primary keyword is filled in, and by kind. */
13
+ /** The filters in the funnel: by the color of the rating, by whether a primary keyword is filled in, and by kind. */
14
14
  ratingFilter: 'Rating',
15
15
  allRatings: 'All ratings',
16
16
  ratings: { good: 'Good (90–100)', average: 'Average (50–89)', bad: 'Poor (0–49)' },
@@ -26,6 +26,9 @@ export const text = {
26
26
  color: 'Color',
27
27
  colors: { accent: 'Accent color', muted: 'Muted text' },
28
28
  noColor: 'No color',
29
+ /** The button for the colour of a section (an H2 and what follows it), and the choices in its menu (packages/editor/lib/section-marker.mjs). */
30
+ sectionColor: 'Section color',
31
+ sectionColors: { none: 'None', surface: 'Surface', tint: 'Tint', accent: 'Accent' },
29
32
  /** The sheet of a link (LinkSheet.tsx). */
30
33
  linkTo: 'The link goes to',
31
34
  linkPage: 'Page',
@@ -16,25 +16,25 @@ export const theme = {
16
16
  lightTheme: 'Light theme',
17
17
  tags: 'Tags',
18
18
  allPosts: 'All',
19
- noPosts: 'There are no posts here yet.',
19
+ noPosts: 'No posts here yet',
20
20
  seeAllPosts: 'See all posts',
21
21
  seeAll: 'See all',
22
22
  blog: 'Blog',
23
23
  relatedPosts: 'Related posts',
24
24
  readingTime: (minutes: number) => `${minutes} min read`,
25
- postsAbout: (count: number, tag: string) => `${count} ${count === 1 ? 'post' : 'posts'} about ${tag}.`,
25
+ postsAbout: (count: number, tag: string) => `${count} ${count === 1 ? 'post' : 'posts'} about ${tag}`,
26
26
  notFound: 'Page not found',
27
27
  didYouMean: 'Did you mean',
28
28
  newPages: 'New pages',
29
29
  newPagesDescription: 'New versions of the pages, for review.',
30
30
  newPagesIntro:
31
- 'New versions of the pages next to the current ones, for review. Google does not see them. When a page is approved, the new text moves to the current page’s address.',
31
+ 'New versions of the pages next to the current ones, for review. Google doesn’t see them. When you approve a page, its new text goes live at the current page’s address.',
32
32
  newVersion: 'New version',
33
- newPage: 'New page, does not exist today',
33
+ newPage: 'New page that doesn’t exist yet',
34
34
  currentPage: (title: string) => `Current: ${title}`,
35
35
  newPosts: 'New blog posts',
36
- newPostsIntro: 'Hidden until they are approved: they are not on the blog, and Google does not see them.',
37
- reviewNotice: 'New version for review. Google does not see it.',
36
+ newPostsIntro: 'Hidden until you approve them. They aren’t on the blog, and Google doesn’t see them.',
37
+ reviewNotice: 'New version for review. Google doesn’t see it.',
38
38
  seeCurrentPage: 'See the current page',
39
39
  seeChanges: 'See what has changed',
40
40
  allNewPages: 'All new pages',
package/src/icon.tsx CHANGED
@@ -1,28 +1,53 @@
1
1
  // Every icon the site draws, on the site and in the editor alike, by its name: Phosphor's (regular, and a filled one as
2
- // `<name>-fill`), which the site's build puts the theme's own in place of (its icons/<name>.svg, else the icon pack in
3
- // use's, packages/editor/site/iconPacks.mjs: themePlugin in packages/editor/site/theme.mjs, which answers the imports below with them). The names are Phosphor's, as
4
- // the content stores them (a card's `icon: certificate`), so a theme without one still draws it.
2
+ // `<name>-fill`), which the site's build puts the theme's own in place of (themePlugin, packages/editor/site/theme.mjs), else
3
+ // a backup from Material Symbols (Outlined) by its Material name (`drag_indicator`, marked data-icon-fallback), else MISSING.
4
+ // Exposes Svg, AnyIcon, Icon, Glyph, SvgFile, MISSING, iconFile, iconNames; uses lib/materialIcon.mjs (the order), materialIcons.ts (lazy set).
5
+ //
6
+ // The names are Phosphor's, as the content stores them (a card's `icon: certificate`), so a theme without one still draws it.
5
7
  // Phosphor's here are those the site draws: the blocks' (src/themes/<theme>/blocks/, src/apps/<app>/blocks/), the
6
8
  // theme's parts' (Icon.astro, MobileMenu.tsx …) and those to pick from (iconNames, and `icons` in collections.ts,
7
- // shown as icons by fields.tsx). A name that is not among them is drawn as MISSING, the question mark, marked with
9
+ // shown as icons by fields.tsx). A name that is not among them is drawn from Material Symbols when it has one (the
10
+ // server render has the whole set at once, virtual:material-symbols-ssr; in the browser it is one lazy chunk, loaded the
11
+ // first time one is drawn); only a name Material Symbols lacks too is MISSING, the question mark, marked with
8
12
  // data-icon-missing (its name), which check:screens finds on the built site's pages and the editor's screens.
9
13
  import { cloneElement, isValidElement, useEffect, useState, type ReactElement } from 'react';
14
+ import { materialName, resolveIcon } from '../lib/materialIcon.mjs';
10
15
  import { svgParts } from '../lib/svg.mjs';
16
+ import ssrMaterial from 'virtual:material-symbols-ssr';
17
+
18
+ const MATERIAL = '/node_modules/@material-symbols/svg-400/outlined/';
19
+ /** Material Symbols' viewBox, every file's. */
20
+ const MATERIAL_VIEWBOX = '0 -960 960 960';
21
+ // Material Symbols' files by their path: at once on the server render, else after the lazy chunk is loaded.
22
+ let materialSet: Record<string, string> | undefined = import.meta.env.SSR ? ssrMaterial : undefined;
23
+ let materialLoading: Promise<void> | undefined;
24
+ const loadMaterial = () =>
25
+ (materialLoading ??= import('./materialIcons').then((m) => {
26
+ materialSet = m.materialIcons;
27
+ }));
28
+ /** A Material Symbols file by `<name>` or `<name>-fill` (a Material name); undefined until the set is loaded. */
29
+ const materialLookup = () => (materialSet ? (key: string) => materialSet?.[`${MATERIAL}${key}.svg`] : undefined);
30
+ /** Draws again when the lazy set has loaded, after it is asked for (`wanted`). */
31
+ function useMaterialLoaded(wanted: boolean) {
32
+ const [, redraw] = useState(0);
33
+ useEffect(() => {
34
+ if (wanted && !materialSet) loadMaterial().then(() => redraw((n) => n + 1));
35
+ }, [wanted]);
36
+ }
11
37
 
12
38
  const PHOSPHOR = '/node_modules/@phosphor-icons/core/assets/';
13
39
  const regular = import.meta.glob<string>(
14
- '/node_modules/@phosphor-icons/core/assets/regular/{arrow-left,arrow-right,arrow-square-out,arrow-up-right,bank,bicycle,book-open,book-open-text,bus,calendar-blank,car,caret-down,caret-left,caret-right,certificate,chats,check,clock,coffee,cookie,copy,device-mobile,door-open,download-simple,envelope-simple,eye,eye-slash,flower-lotus,graduation-cap,hand-heart,handshake,heart,house,info,keyboard,leaf,lifebuoy,lightbulb,list,magnifying-glass,map-pin,map-trifold,medal,minus,moon,pencil-simple,phone,question,sun,ticket,train,trash,user,users,users-three,video-camera,warning-circle,wheelchair,x}.svg',
40
+ '/node_modules/@phosphor-icons/core/assets/regular/{arrow-left,arrow-right,arrow-square-out,arrow-up-right,bank,bicycle,book-open,book-open-text,bus,calendar-blank,car,caret-down,caret-left,caret-right,caret-up,certificate,chats,check,clock,coffee,cookie,copy,device-mobile,door-open,download-simple,envelope-simple,eye,eye-slash,flower-lotus,graduation-cap,hand-heart,handshake,heart,house,info,keyboard,leaf,lifebuoy,lightbulb,list,magnifying-glass,map-pin,map-trifold,medal,minus,moon,pencil-simple,phone,question,sun,ticket,train,trash,user,users,users-three,video-camera,warning-circle,wheelchair,x}.svg',
41
+ { query: '?raw', import: 'default', eager: true },
42
+ );
43
+ // The filled ones ("Video": its play button; the navigation drawer's active item).
44
+ const fills = import.meta.glob<string>(
45
+ '/node_modules/@phosphor-icons/core/assets/fill/{browser-fill,buildings-fill,certificate-fill,chart-line-up-fill,chats-fill,envelope-simple-fill,file-text-fill,folder-simple-fill,heart-fill,list-checks-fill,magnifying-glass-fill,megaphone-fill,newspaper-fill,paint-brush-broad-fill,play-fill,puzzle-piece-fill,shield-check-fill,square-half-bottom-fill,squares-four-fill,storefront-fill,tag-fill,toggle-right-fill,trash-fill,user-fill,wrench-fill}.svg',
15
46
  { query: '?raw', import: 'default', eager: true },
16
47
  );
17
- // The filled ones ("Video": its play button).
18
- const fills = import.meta.glob<string>('/node_modules/@phosphor-icons/core/assets/fill/{play-fill}.svg', {
19
- query: '?raw',
20
- import: 'default',
21
- eager: true,
22
- });
23
48
 
24
- /** The icon drawn for one that is not found (a name the site does not draw, one Phosphor does not have): a question
25
- * mark, the icon pack's as the others. */
49
+ /** The icon drawn for one that is not found (a name the site does not draw, one Material Symbols does not have either): a
50
+ * question mark, the icon pack's as the others. */
26
51
  export const MISSING = 'question';
27
52
 
28
53
  /** An icon's SVG file by its name: the theme's, else Phosphor's; undefined for one the site does not draw. */
@@ -32,7 +57,8 @@ export function iconFile(name: string, weight?: 'fill'): string | undefined {
32
57
 
33
58
  /** An icon by name as the site and the editor draw it (the theme's View.tsx and its parts, Icon.astro, the editor's
34
59
  * fields): the bare <svg> in its file's viewBox, at the size given, in the text's colour, hidden from screen readers
35
- * (or named by `label`). A filled one the site does not draw is the regular one, and an icon it does not draw MISSING. */
60
+ * (or named by `label`). A filled one the site does not draw is the regular one; an icon it does not draw is Material
61
+ * Symbols' by its Material name (marked data-icon-fallback), and MISSING when it has none. */
36
62
  export function Svg({
37
63
  name,
38
64
  size = 20,
@@ -46,27 +72,56 @@ export function Svg({
46
72
  className?: string;
47
73
  label?: string;
48
74
  }) {
49
- const file = iconFile(name, weight) ?? iconFile(name);
50
- return <Drawn file={file} missing={file ? undefined : name} size={size} className={className} label={label} />;
75
+ const found = resolveIcon(name, weight, { list: iconFile, material: materialLookup() });
76
+ useMaterialLoaded(!!found.pending);
77
+ return (
78
+ <Drawn
79
+ file={found.file}
80
+ fallback={found.fallback}
81
+ pending={found.pending}
82
+ missing={found.file || found.pending ? undefined : name}
83
+ size={size}
84
+ className={className}
85
+ label={label}
86
+ />
87
+ );
51
88
  }
52
89
 
53
90
  /** An icon by its name of all Phosphor's, not only those the site draws (an app's in the app store, store.ts): one the
54
91
  * site draws at once, another from all Phosphor's (allIcons.ts, loaded the first time one is drawn), the theme's in
55
- * their place as Svg's. Nothing while it loads, and MISSING for a name Phosphor does not have. */
92
+ * their place as Svg's. Then Material Symbols' by its Material name (marked data-icon-fallback). Nothing while it loads, and
93
+ * MISSING for a name neither has. */
56
94
  export function AnyIcon({ name, size = 20 }: { name: string; size?: number }) {
57
- // null: not found.
58
- const [file, setFile] = useState<string | null | undefined>(() => iconFile(name));
95
+ // file null: not found; fallback: the file is Material Symbols'.
96
+ const [found, setFound] = useState<{ file: string | null; fallback?: boolean } | undefined>(() => {
97
+ const file = iconFile(name);
98
+ return file ? { file } : undefined;
99
+ });
59
100
  useEffect(() => {
60
- if (iconFile(name)) return setFile(iconFile(name));
61
- setFile(undefined);
101
+ const own = iconFile(name);
102
+ if (own) return setFound({ file: own });
103
+ setFound(undefined);
62
104
  let on = true;
63
- import('./allIcons').then((m) => on && setFile(m.allIcons[`${PHOSPHOR}regular/${name}.svg`] ?? null));
105
+ (async () => {
106
+ const phosphor = (await import('./allIcons')).allIcons[`${PHOSPHOR}regular/${name}.svg`];
107
+ if (phosphor) return on && setFound({ file: phosphor });
108
+ if (materialName(name)) await loadMaterial();
109
+ const backup = resolveIcon(name, undefined, { list: () => undefined, material: materialLookup() });
110
+ if (on) setFound(backup.file ? { file: backup.file, fallback: true } : { file: null });
111
+ })();
64
112
  return () => {
65
113
  on = false;
66
114
  };
67
115
  }, [name]);
68
- if (file === undefined) return null;
69
- return <Drawn file={file ?? undefined} missing={file ? undefined : name} size={size} />;
116
+ if (found === undefined) return null;
117
+ return (
118
+ <Drawn
119
+ file={found.file ?? undefined}
120
+ fallback={found.fallback ? name : undefined}
121
+ missing={found.file ? undefined : name}
122
+ size={size}
123
+ />
124
+ );
70
125
  }
71
126
 
72
127
  /** An icon's SVG file drawn as it is, as Svg draws one, not the theme's in its place: an icon pack's own, to show what the
@@ -75,22 +130,28 @@ export function SvgFile({ file, size = 20 }: { file: string; size?: number }) {
75
130
  return <Drawn file={file} size={size} />;
76
131
  }
77
132
 
78
- /** An SVG file drawn; MISSING in its place when there is none, or it is not an SVG (`missing`: the name asked for). */
133
+ /** An SVG file drawn; MISSING in its place when there is none, or it is not an SVG (`missing`: the name asked for).
134
+ * `fallback`: the name asked for, when the file is Material Symbols' (data-icon-fallback). `pending`: a Material icon
135
+ * not loaded yet: the empty <svg> (the server render's own content stays while the page hydrates). */
79
136
  function Drawn({
80
137
  file,
138
+ fallback,
139
+ pending,
81
140
  missing,
82
141
  size,
83
142
  className,
84
143
  label,
85
144
  }: {
86
145
  file: string | undefined;
146
+ fallback?: string;
147
+ pending?: boolean;
87
148
  missing?: string;
88
149
  size: number;
89
150
  className?: string;
90
151
  label?: string;
91
152
  }) {
92
153
  const parts = svgParts(file);
93
- const shown = parts ?? svgParts(iconFile(MISSING));
154
+ const shown = pending ? { viewBox: MATERIAL_VIEWBOX, inner: '' } : (parts ?? svgParts(iconFile(MISSING)));
94
155
  if (!shown) return null;
95
156
  return (
96
157
  <svg
@@ -98,7 +159,9 @@ function Drawn({
98
159
  height={size}
99
160
  fill="currentColor"
100
161
  {...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })}
101
- {...(parts ? {} : { 'data-icon-missing': missing ?? '' })}
162
+ {...(parts || pending ? {} : { 'data-icon-missing': missing ?? '' })}
163
+ {...(parts && fallback !== undefined ? { 'data-icon-fallback': fallback } : {})}
164
+ suppressHydrationWarning={pending}
102
165
  className={className}
103
166
  xmlns="http://www.w3.org/2000/svg"
104
167
  viewBox={shown.viewBox}
@@ -0,0 +1,28 @@
1
+ // Tests for splitCms in src/licenseData.ts: the CMS's own licence is pulled out of the works the editor and the site bring.
2
+ // Run: node --test packages/editor/src/licenseData.test.mjs
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { PACKAGE } from '../lib/cms-version.mjs';
6
+ import { splitCms } from './licenseData.ts';
7
+
8
+ const work = (id, version = '1.0.0') => ({ id, name: id, version });
9
+
10
+ test('the CMS is pulled out and the rest keeps its order', () => {
11
+ const { cms, rest } = splitCms([work('a'), work(PACKAGE), work('b'), work('c')]);
12
+ assert.equal(cms.id, PACKAGE);
13
+ assert.deepEqual(
14
+ rest.map((w) => w.id),
15
+ ['a', 'b', 'c'],
16
+ );
17
+ });
18
+
19
+ test('without the CMS, cms is null and the rest is unchanged', () => {
20
+ const works = [work('a'), work('b')];
21
+ const { cms, rest } = splitCms(works);
22
+ assert.equal(cms, null);
23
+ assert.deepEqual(rest, works);
24
+ });
25
+
26
+ test('the CMS keeps its version', () => {
27
+ assert.equal(splitCms([work(PACKAGE, '0.1.0')]).cms.version, '0.1.0');
28
+ });
@@ -1,7 +1,9 @@
1
1
  // The licences of the third-party work the site and the editor are made with ("Licenser" under "Indstillinger",
2
2
  // Licenses.tsx), as the build finds them (/editor/licenses.json, licensesIntegration in packages/editor/lib/licenses.mjs),
3
3
  // what an app (defineApp) and a theme (theme.json) declare (their own licence and the third-party work they bring), and
4
- // what the store's catalog has of them (scripts/store-release.mjs, storeLicenses).
4
+ // what the store's catalog has of them (scripts/store-release.mjs, storeLicenses). Also splits the CMS's own licence out
5
+ // of the works the editor and the site bring (splitCms), to be shown first. Depends on PACKAGE (lib/cms-version.mjs).
6
+ import { PACKAGE } from '../lib/cms-version.mjs';
5
7
 
6
8
  /** What a third-party work is to the site: a font, icons, code (a library, also styles) or pictures. */
7
9
  export type LicenseKind = 'font' | 'icons' | 'library' | 'image';
@@ -77,3 +79,12 @@ export function loadLicenses(): Promise<LicensesFile | null> {
77
79
  .catch(() => null);
78
80
  return loaded;
79
81
  }
82
+
83
+ /** The works the editor and the site bring, split in the CMS's own licence (the work with the CMS's package name, none
84
+ * when the build did not find it) and the rest, which keeps its order. */
85
+ export function splitCms<W extends Pick<LicensedWork, 'id'>>(works: W[]): { cms: W | null; rest: W[] } {
86
+ return { cms: works.find((w) => w.id === PACKAGE) ?? null, rest: works.filter((w) => w.id !== PACKAGE) };
87
+ }
88
+
89
+ /** The CMS's name in its own licence row ("Morpheme CMS itself"); a product name, so it is not translated. */
90
+ export const CMS_NAME = 'Morpheme CMS';
@@ -8,5 +8,7 @@ export const HTML: "html";
8
8
  export const siteBlockSchema: import("@milkdown/kit/utils").$NodeSchema<"siteBlock">;
9
9
  /** @type {import('@milkdown/kit/utils').$MarkSchema<'textColor'>} */
10
10
  export const textColorSchema: import("@milkdown/kit/utils").$MarkSchema<"textColor">;
11
+ /** @type {import('@milkdown/kit/utils').$NodeSchema<'heading'>} */
12
+ export const headingWithSection: import("@milkdown/kit/utils").$NodeSchema<"heading">;
11
13
  /** @type {import('@milkdown/kit/utils').$Prose} */
12
14
  export const spacing: import("@milkdown/kit/utils").$Prose;