@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613

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 (186) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/astro.mjs +2 -1
  3. package/defaults/blockTexts.ts +9 -9
  4. package/defaults/collections.ts +257 -257
  5. package/defaults/colors.ts +74 -74
  6. package/defaults/company.ts +9 -9
  7. package/defaults/editor.ts +65 -65
  8. package/defaults/i18n/da-DK/blockTexts.ts +12 -0
  9. package/defaults/i18n/da-DK/collections.ts +220 -0
  10. package/defaults/i18n/da-DK/colors.ts +102 -0
  11. package/defaults/i18n/da-DK/company.ts +7 -0
  12. package/defaults/i18n/da-DK/editor.ts +13 -0
  13. package/defaults/i18n/da-DK/index.ts +31 -0
  14. package/defaults/i18n/da-DK/internalLinks.ts +12 -0
  15. package/defaults/i18n/da-DK/opens.ts +7 -0
  16. package/defaults/i18n/da-DK/popup.ts +9 -0
  17. package/defaults/i18n/da-DK/privacy.ts +38 -0
  18. package/defaults/i18n/da-DK/rules.ts +56 -0
  19. package/defaults/i18n/da-DK/sitedata.ts +8 -0
  20. package/defaults/i18n/da-DK/standard.ts +8 -0
  21. package/defaults/i18n/da-DK/standardPages.ts +17 -0
  22. package/defaults/internalLinks.ts +8 -8
  23. package/defaults/opens.ts +2 -2
  24. package/defaults/popup.tsx +7 -7
  25. package/defaults/privacy.ts +37 -37
  26. package/defaults/rules.ts +68 -66
  27. package/defaults/schema.ts +1 -1
  28. package/defaults/sitedata.ts +4 -4
  29. package/defaults/standard.ts +9 -6
  30. package/defaults/standardPages.ts +14 -14
  31. package/defaults/texts.ts +7 -510
  32. package/lib/appId.d.mts +1 -0
  33. package/lib/appId.mjs +6 -0
  34. package/lib/appRoute.d.mts +2 -2
  35. package/lib/appRoute.mjs +3 -3
  36. package/lib/appRoute.test.mjs +8 -8
  37. package/lib/apps.mjs +13 -3
  38. package/lib/apps.test.mjs +10 -1
  39. package/lib/contracts.d.mts +3 -2
  40. package/lib/contracts.mjs +3 -2
  41. package/lib/folderChanges.d.mts +29 -0
  42. package/lib/folderChanges.mjs +34 -0
  43. package/lib/licenses.mjs +2 -2
  44. package/lib/licenses.owner.test.mjs +9 -0
  45. package/lib/menu.mjs +4 -4
  46. package/lib/menu.test.mjs +101 -101
  47. package/lib/scheduled.mjs +1 -1
  48. package/lib/section-ranges.d.mts +1 -1
  49. package/lib/section-ranges.mjs +8 -14
  50. package/lib/section-ranges.test.mjs +26 -5
  51. package/lib/storeEntries.d.mts +3 -1
  52. package/lib/storeEntries.mjs +22 -8
  53. package/lib/storeEntries.test.mjs +59 -7
  54. package/lib/text-sections.d.mts +11 -1
  55. package/lib/text-sections.mjs +18 -8
  56. package/lib/text-sections.test.mjs +34 -1
  57. package/lib/translate.d.mts +11 -0
  58. package/lib/translate.mjs +13 -1
  59. package/lib/translate.test.mjs +23 -2
  60. package/lib/video-id.d.mts +6 -2
  61. package/lib/video-id.mjs +5 -1
  62. package/lib/video.d.mts +6 -2
  63. package/lib/video.mjs +5 -1
  64. package/lib/without-h1.d.mts +1 -0
  65. package/lib/without-h1.mjs +19 -4
  66. package/lib/without-h1.test.mjs +30 -1
  67. package/package.json +3 -2
  68. package/scripts/app-screenshots.mjs +9 -8
  69. package/scripts/check-content.mjs +12 -2
  70. package/scripts/check-language.mjs +0 -1
  71. package/scripts/check-site-types.mjs +26 -0
  72. package/scripts/editor-screens.mjs +3 -3
  73. package/scripts/site-types.mjs +14 -0
  74. package/scripts/site-types.test.mjs +21 -0
  75. package/site/activeTheme.test.mjs +52 -0
  76. package/site/appSelf.d.mts +2 -0
  77. package/site/appSelf.mjs +23 -0
  78. package/site/appSelf.test.mjs +35 -0
  79. package/site/content.ts +5 -2
  80. package/site/defineSite.mjs +2 -2
  81. package/site/schema.ts +3 -3
  82. package/site/siteModules.mjs +2 -1
  83. package/site/theme.mjs +19 -8
  84. package/src/Advanced.tsx +2 -2
  85. package/src/App.tsx +79 -78
  86. package/src/AppAdds.tsx +1 -1
  87. package/src/AppList.tsx +39 -24
  88. package/src/BlockDialog.tsx +39 -18
  89. package/src/BlockParts.tsx +6 -6
  90. package/src/BlockSettings.tsx +2 -2
  91. package/src/BrandForm.tsx +2 -2
  92. package/src/Colors.tsx +6 -6
  93. package/src/CompanyForm.tsx +1 -1
  94. package/src/Consents.tsx +3 -3
  95. package/src/CreateFab.tsx +8 -0
  96. package/src/DataForm.tsx +10 -20
  97. package/src/Deleted.tsx +3 -3
  98. package/src/EditorFooter.tsx +2 -2
  99. package/src/EntryForm.tsx +3 -3
  100. package/src/FooterSettings.tsx +2 -2
  101. package/src/FormFrame.tsx +4 -3
  102. package/src/HeaderSettings.tsx +3 -3
  103. package/src/Keywords.tsx +5 -5
  104. package/src/Library.tsx +75 -82
  105. package/src/Licenses.tsx +8 -8
  106. package/src/ListFab.tsx +13 -0
  107. package/src/MainDrawerParts.tsx +3 -3
  108. package/src/MainMenu.tsx +1 -1
  109. package/src/MarkdownEditor.tsx +88 -47
  110. package/src/Media.tsx +11 -20
  111. package/src/MediaTags.tsx +2 -2
  112. package/src/Notifications.tsx +2 -2
  113. package/src/Parts.tsx +17 -17
  114. package/src/Redirects.tsx +14 -22
  115. package/src/Screen.tsx +16 -6
  116. package/src/SeoScores.tsx +6 -6
  117. package/src/SideColumn.tsx +2 -2
  118. package/src/StartFab.tsx +6 -8
  119. package/src/ThemeStore.tsx +37 -16
  120. package/src/Tokens.tsx +2 -2
  121. package/src/apps.ts +7 -8
  122. package/src/checkSite.ts +86 -20
  123. package/src/drawerYield.d.mts +3 -0
  124. package/src/drawerYield.mjs +10 -0
  125. package/src/drawerYield.test.mjs +22 -0
  126. package/src/editor.css +59 -38
  127. package/src/examples.ts +1 -1
  128. package/src/fabScreens.test.mjs +28 -0
  129. package/src/fabScreens.ts +16 -0
  130. package/src/i18n/da-DK/library.ts +2 -2
  131. package/src/i18n/da-DK/text.ts +1 -1
  132. package/src/i18n/en-US/apps.ts +1 -1
  133. package/src/i18n/en-US/common.ts +8 -8
  134. package/src/i18n/en-US/library.ts +5 -5
  135. package/src/i18n/en-US/parts.ts +3 -3
  136. package/src/i18n/en-US/text.ts +1 -1
  137. package/src/i18n/index.ts +4 -8
  138. package/src/images.ts +2 -2
  139. package/src/legacyMove.test.mjs +137 -0
  140. package/src/legacyMove.ts +131 -0
  141. package/src/licenseData.ts +3 -3
  142. package/src/markdown.mjs +10 -7
  143. package/src/markdown.test.mjs +16 -2
  144. package/src/menuChildren.ts +5 -5
  145. package/src/menuOpen.ts +20 -4
  146. package/src/movedRoutes.d.mts +8 -1
  147. package/src/movedRoutes.mjs +102 -21
  148. package/src/movedRoutes.test.mjs +128 -6
  149. package/src/nav.ts +1 -1
  150. package/src/notificationData.ts +1 -1
  151. package/src/pageH1Panel.d.mts +4 -0
  152. package/src/pageH1Panel.mjs +31 -0
  153. package/src/pageH1Panel.test.mjs +56 -0
  154. package/src/pagePreview.tsx +22 -12
  155. package/src/schema.ts +2 -2
  156. package/src/sectionFrame.ts +3 -3
  157. package/src/sectionState.d.mts +3 -3
  158. package/src/sectionState.mjs +6 -6
  159. package/src/sectionState.test.mjs +14 -2
  160. package/src/sheetPlacement.d.mts +6 -4
  161. package/src/sheetPlacement.mjs +15 -10
  162. package/src/sheetPlacement.test.mjs +16 -9
  163. package/src/sidePanel.ts +38 -0
  164. package/src/site.ts +37 -13
  165. package/src/standard/accordion/index.tsx +1 -1
  166. package/src/standard/accordion/shared.ts +1 -1
  167. package/src/standard/buttons/index.tsx +1 -1
  168. package/src/standard/cards/index.tsx +1 -1
  169. package/src/standard/cards/shared.ts +1 -1
  170. package/src/standard/cookies/index.tsx +1 -1
  171. package/src/standard/posts/index.tsx +1 -1
  172. package/src/standard/testimonials/index.tsx +1 -1
  173. package/src/standard/testimonials/list.ts +1 -1
  174. package/src/store.ts +36 -31
  175. package/src/tagUses.d.mts +1 -1
  176. package/src/tagUses.mjs +2 -2
  177. package/src/tagUses.test.mjs +1 -1
  178. package/src/topBarSlot.tsx +7 -15
  179. package/src/ui.tsx +33 -22
  180. package/src/virtual.d.ts +6 -0
  181. package/starter/package.json +1 -1
  182. package/theme/components/PageHeading.astro +22 -0
  183. package/theme/layouts/Base.astro +2 -2
  184. package/theme/layouts/PageLayout.astro +37 -20
  185. package/theme/styles/global.css +2 -1
  186. package/theme/styles/ui.css +40 -10
@@ -1,7 +1,8 @@
1
1
  // The text of a page or blog post: Milkdown (packages/editor/src/markdown.mjs) with a small toolbar.
2
2
  // The text is shown in the site's own style (the "prose" classes of the pages). On pages the site's
3
3
  // blocks (src/apps/<app>/blocks/) can be put in the text with a button; each is a card with the block as it looks
4
- // on the site, which opens a menu (Ret, hide on the site, move, Fjern), with a free line kept between
4
+ // on the site, which opens a menu (hide on the site, move; below expanded also Ret, Fjern; from expanded a tap opens the
5
+ // side panel instead), with a free line kept between
5
6
  // two cards to write in. A fixed page's sections are such a text too (SectionsField.tsx), so both look
6
7
  // and work the same. The last tool, "Section settings", opens the section's sheet (BlockDialog.tsx).
7
8
  import {
@@ -64,7 +65,8 @@ import { sectionFrame } from './sectionFrame';
64
65
  import { IMAGE, type BlockAsk } from './BlockDialog';
65
66
  import SectionSettingsIcon from './SectionSettingsIcon';
66
67
  import { askLink } from './LinkSheet';
67
- import { Menu, MenuItem, Toolbar, ToolbarButton } from './ui';
68
+ import { atLeastNow, Menu, MenuItem, Toolbar, ToolbarButton } from './ui';
69
+ import { editsInPanel } from './sheetPlacement.mjs';
68
70
  import { shownAt } from './images';
69
71
  import { cropStyle, joinPicture, splitPicture } from '../lib/crop.mjs';
70
72
  import { describe, htmlBlock, type BlockProps } from './blocks';
@@ -283,8 +285,16 @@ const moves = (view: EditorView, i: number | null, locked: string[] = []): CardI
283
285
  };
284
286
 
285
287
  // The row of icons on a card (a block or a picture) in the text, opened by tapping it: items, as
286
- // they are when it opens. Returns how to close it.
287
- function cardMenu(view: EditorView, dom: HTMLElement, card: HTMLElement, name: string, items: () => (CardItem | undefined | '')[]) {
288
+ // they are when it opens (no row when there are none). `tap` runs first on a tap that opens the row (not on one that
289
+ // closes it): from expanded it opens the card's side panel. Returns how to close it.
290
+ function cardMenu(
291
+ view: EditorView,
292
+ dom: HTMLElement,
293
+ card: HTMLElement,
294
+ name: string,
295
+ items: () => (CardItem | undefined | false | '')[],
296
+ tap?: () => void,
297
+ ) {
288
298
  let menu: HTMLElement | null = null;
289
299
  function close() {
290
300
  menu?.remove();
@@ -304,12 +314,13 @@ function cardMenu(view: EditorView, dom: HTMLElement, card: HTMLElement, name: s
304
314
  // `focus` (the arrow used to move the card, when it opens again on the moved card).
305
315
  function open(keyboard: boolean, focus?: string) {
306
316
  closeOpenMenu?.();
317
+ const shown = items().filter((it): it is CardItem => !!it);
318
+ if (!shown.length) return;
307
319
  menu = dom.appendChild(document.createElement('div'));
308
320
  menu.className = 'site-block-menu';
309
321
  menu.setAttribute('role', 'menu');
310
322
  menu.setAttribute('aria-label', name);
311
- for (const it of items()) {
312
- if (!it) continue;
323
+ for (const it of shown) {
313
324
  const b = menu.appendChild(document.createElement('button'));
314
325
  b.type = 'button';
315
326
  b.setAttribute('role', 'menuitem');
@@ -356,7 +367,11 @@ function cardMenu(view: EditorView, dom: HTMLElement, card: HTMLElement, name: s
356
367
  document.addEventListener('pointercancel', unlock, { capture: true, once: true });
357
368
  timer = window.setTimeout(unlock, 1000);
358
369
  });
359
- card.addEventListener('click', (e) => (menu ? close() : open(e.detail === 0)));
370
+ card.addEventListener('click', (e) => {
371
+ if (menu) return close();
372
+ tap?.();
373
+ open(e.detail === 0);
374
+ });
360
375
  return close;
361
376
  }
362
377
 
@@ -397,7 +412,8 @@ function pictureCard(node: Node, view: EditorView, getPos: () => number | undefi
397
412
  if (cut) img.style.cssText = cut.img;
398
413
  if (node.attrs.title) img.title = node.attrs.title;
399
414
  const describe = async () => {
400
- const props = await sheet()?.edit(IMAGE, { src, alt: node.attrs.alt ?? '', format, crop, decorative });
415
+ const props = await sheet()?.edit(IMAGE, { src, alt: node.attrs.alt ?? '', format, crop, decorative }, false, true);
416
+ if (props === 'remove') return remove();
401
417
  const pos = getPos();
402
418
  if (!props || pos === undefined) return;
403
419
  view.dispatch(
@@ -408,14 +424,23 @@ function pictureCard(node: Node, view: EditorView, getPos: () => number | undefi
408
424
  const pos = getPos();
409
425
  if (pos !== undefined) view.dispatch(view.state.tr.delete(pos, pos + node.nodeSize));
410
426
  };
411
- const close = cardMenu(view, dom, card, t.text.image, () => {
412
- const pos = getPos();
413
- return [
414
- { icon: pencilIcon, label: t.common.edit, action: describe },
415
- ...moves(view, pos === undefined ? null : view.state.doc.resolve(pos).index(0), sheet()?.template?.locked),
416
- { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
417
- ];
418
- });
427
+ // From expanded a picture's sheet is the side panel: a tap opens it, and the menu has no pen or trash (Remove is in the panel).
428
+ const inPanelNow = () => editsInPanel(atLeastNow('expanded'), true);
429
+ const close = cardMenu(
430
+ view,
431
+ dom,
432
+ card,
433
+ t.text.image,
434
+ () => {
435
+ const pos = getPos();
436
+ return [
437
+ !inPanelNow() && { icon: pencilIcon, label: t.common.edit, action: describe },
438
+ ...moves(view, pos === undefined ? null : view.state.doc.resolve(pos).index(0), sheet()?.template?.locked),
439
+ !inPanelNow() && { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
440
+ ];
441
+ },
442
+ () => inPanelNow() && void describe(),
443
+ );
419
444
  return { dom, update: () => false, stopEvent: () => true, ignoreMutation: () => true, destroy: close };
420
445
  }
421
446
 
@@ -468,7 +493,7 @@ async function editLink(view: EditorView) {
468
493
  /** What applies where the cursor is, or to all of the selection: 'strong', 'emphasis', 'link',
469
494
  * 'h2', 'h3', 'text' (a paragraph, not in a list or quote), 'bullet', 'ordered' and 'quote'; with a colour
470
495
  * (text-colors.mjs) 'textColor' and the colour's class ('color-accent'). For the toolbar and the selection's menu,
471
- * 'section' and 'section-<colour>' in a top-level H2 (sectionState.mjs), which mark them. */
496
+ * 'section' and 'section-<colour>' in a top-level H1 or H2 (sectionState.mjs), which mark them. */
472
497
  export function activeOf(state: EditorView['state']): Set<string> {
473
498
  const { selection, storedMarks, schema } = state;
474
499
  const { $from, from, to, empty } = selection;
@@ -528,11 +553,11 @@ type Look = {
528
553
  // The levels of the top-level headings of a document, in order.
529
554
  const levelsOf = (doc: EditorView['state']['doc']): number[] => topHeadings(doc).map(({ node }) => node.attrs.level);
530
555
 
531
- // Whether a heading (H2) is selected, or a part of one.
532
- function inH2(state: EditorView['state']) {
556
+ // Whether a heading that starts a section (H1 or H2) is selected, or a part of one.
557
+ function inSectionHeading(state: EditorView['state']) {
533
558
  let found = false;
534
559
  const { from, to } = state.selection;
535
- state.doc.nodesBetween(from, to, (n) => void (found ||= n.type.name === 'heading' && n.attrs.level === 2));
560
+ state.doc.nodesBetween(from, to, (n) => void (found ||= n.type.name === 'heading' && n.attrs.level <= 2));
536
561
  return found;
537
562
  }
538
563
 
@@ -544,7 +569,7 @@ const inLabel = (state: EditorView['state']) => state.selection.$from.parent.typ
544
569
  // page's headings (`h1`: only the levels headingChoices gives for the text), a heading or a subheading, a label above the heading; the ones that apply are marked. At a still
545
570
  // cursor, bold, italic, link and colour act on the whole word, the headings on the line; a move or a keypress hides the
546
571
  // row. On a phone, where the phone's own menu is over the selection and its handles under it, the row is at the bottom
547
- // of the screen, over the keyboard. sections: no colour in a heading (H2), the name of a section.
572
+ // of the screen, over the keyboard. sections: no colour in a heading (H1 or H2), the name of a section.
548
573
  function selectionMenu(ctx: Ctx, h1: boolean, sections: boolean, colors: (button: HTMLElement) => void) {
549
574
  return new Plugin({
550
575
  view(view) {
@@ -588,7 +613,7 @@ function selectionMenu(ctx: Ctx, h1: boolean, sections: boolean, colors: (button
588
613
  label: t.text.color,
589
614
  on: (s) => marked(s, 'textColor'),
590
615
  action: colors,
591
- off: (s) => inLabel(s) || (sections && inH2(s)),
616
+ off: (s) => inLabel(s) || (sections && inSectionHeading(s)),
592
617
  word: true,
593
618
  },
594
619
  ...(h1 ? [1, 2, 3] : [2, 3]).map((level) => ({
@@ -783,8 +808,20 @@ function blockCard(
783
808
  const pos = getPos();
784
809
  return pos === undefined ? null : view.state.doc.resolve(pos).index(0);
785
810
  };
811
+ // What the template (collections.ts) says of this block: a locked one is only edited, and the last shown one of a block
812
+ // the page must have cannot be hidden or removed (stays).
813
+ const keeps = () => {
814
+ const rules = asker()?.template;
815
+ const type = node.attrs.type;
816
+ const locked = !!rules?.locked?.includes(type);
817
+ const shownOfType = blocksOf(view.state.doc).filter((b) => b.type === type && b.hidden === false).length;
818
+ // (Not written with "!" before "hidden", which Tailwind would take for a class for the site.)
819
+ const last = hidden === false && !!rules?.required?.includes(type) && shownOfType <= 1;
820
+ return { rules, locked, stays: locked || last };
821
+ };
786
822
  const edit = async () => {
787
- const props = await asker()?.edit(node.attrs.type, node.attrs.props);
823
+ const props = await asker()?.edit(node.attrs.type, node.attrs.props, false, !keeps().stays);
824
+ if (props === 'remove') return remove();
788
825
  const here = at();
789
826
  // The block's fields as changed; still hidden if it was.
790
827
  if (props && here)
@@ -806,26 +843,29 @@ function blockCard(
806
843
  // The choices that can be made here: no "Ret" without fields (but to where it is edited instead,
807
844
  // Block.edit), no move past the start or end. By the template (collections.ts): a locked block is
808
845
  // only edited, and the last shown one of a block the page must have cannot be hidden or removed.
809
- const close = cardMenu(view, dom, card, name, () => {
810
- const i = top();
811
- const rules = asker()?.template;
812
- const type = node.attrs.type;
813
- const locked = !!rules?.locked?.includes(type);
814
- const shownOfType = blocksOf(view.state.doc).filter((b) => b.type === type && b.hidden === false).length;
815
- // (Not written with "!" before "hidden", which Tailwind would take for a class for the site.)
816
- const last = hidden === false && !!rules?.required?.includes(type) && shownOfType <= 1;
817
- const stays = locked || last;
818
- return [
819
- block?.fields.length
820
- ? { icon: pencilIcon, label: t.common.edit, action: edit }
821
- : block?.edit && { icon: pencilIcon, label: block.edit.text, action: () => leaveTo(`#${block.edit!.to}`) },
822
- block?.shown || (stays && hidden === false)
823
- ? undefined
824
- : { icon: hidden ? eyeIcon : eyeSlashIcon, label: hidden ? t.text.showOnPage : t.text.hideOnPage, action: toggle },
825
- ...(locked ? [] : moves(view, i, rules?.locked)),
826
- stays ? undefined : { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
827
- ];
828
- });
846
+ // From expanded the block's fields are in the side panel: a tap opens it, and the menu has no pen or trash (Remove is in the panel).
847
+ const inPanelNow = () => editsInPanel(atLeastNow('expanded'), !!block?.fields.length);
848
+ const close = cardMenu(
849
+ view,
850
+ dom,
851
+ card,
852
+ name,
853
+ () => {
854
+ const i = top();
855
+ const { rules, locked, stays } = keeps();
856
+ return [
857
+ block?.fields.length
858
+ ? !inPanelNow() && { icon: pencilIcon, label: t.common.edit, action: edit }
859
+ : block?.edit && { icon: pencilIcon, label: block.edit.text, action: () => leaveTo(`#${block.edit!.to}`) },
860
+ block?.shown || (stays && hidden === false)
861
+ ? undefined
862
+ : { icon: hidden ? eyeIcon : eyeSlashIcon, label: hidden ? t.text.showOnPage : t.text.hideOnPage, action: toggle },
863
+ ...(locked ? [] : moves(view, i, rules?.locked)),
864
+ stays || inPanelNow() ? undefined : { icon: trashIcon, label: t.common.remove, action: remove, danger: true },
865
+ ];
866
+ },
867
+ () => inPanelNow() && void edit(),
868
+ );
829
869
  // A changed block is drawn anew (update false); taps on the card and its menu are not ProseMirror's.
830
870
  return {
831
871
  dom,
@@ -1039,7 +1079,7 @@ export default forwardRef<MarkdownHandle, Props>(function MarkdownEditor(
1039
1079
  if (!src) return;
1040
1080
  const sheet = blocks ?? sheetProp;
1041
1081
  const picked = await sheet?.edit(IMAGE, { src, alt: '' }, true);
1042
- if (picked) run(insertImageCommand, { src: pictureSrc(picked), alt: String(picked.alt ?? '').trim() });
1082
+ if (picked && picked !== 'remove') run(insertImageCommand, { src: pictureSrc(picked), alt: String(picked.alt ?? '').trim() });
1043
1083
  }
1044
1084
 
1045
1085
  // Puts the chosen block where the cursor is (a paragraph is split around it), or after the block
@@ -1113,8 +1153,9 @@ export default forwardRef<MarkdownHandle, Props>(function MarkdownEditor(
1113
1153
  : []),
1114
1154
  { label: t.text.undo, icon: ArrowUUpLeft, action: () => run(undoCommand) },
1115
1155
  { label: t.text.redo, icon: ArrowUUpRight, action: () => run(redoCommand) },
1116
- // The settings of the section the cursor's H2 starts (not a fixed page's text, whose H2s are its sections' names; the front page has it).
1117
- ...(framed && active.includes('section') && (blocks ?? sheetProp)
1156
+ // The settings of the section the cursor's H1 or H2 starts (not a fixed page's text, whose H2s are its sections' names; the front
1157
+ // page has it for its H2s, but not for its H1, which stays the banner without a colour).
1158
+ ...(framed && active.includes('section') && !(sections && active.includes('h1')) && (blocks ?? sheetProp)
1118
1159
  ? [{ label: t.text.sectionSettings, icon: SectionSettingsIcon, action: sectionSettings }]
1119
1160
  : []),
1120
1161
  ];
@@ -1168,7 +1209,7 @@ export default forwardRef<MarkdownHandle, Props>(function MarkdownEditor(
1168
1209
  <Toolbar
1169
1210
  ref={toolbar}
1170
1211
  label={t.text.tools}
1171
- className="editor-tools sticky top-[calc(5rem+1px+env(safe-area-inset-top))] z-10 m-2 justify-center-safe overflow-x-auto rounded-panel border border-line medium:flex-wrap"
1212
+ className="editor-tools sticky top-[env(safe-area-inset-top)] z-10 m-2 justify-center-safe overflow-x-auto rounded-panel border border-line medium:flex-wrap"
1172
1213
  >
1173
1214
  {tools.map((tool) => (
1174
1215
  <ReactFragment key={tool.label}>
package/src/Media.tsx CHANGED
@@ -7,27 +7,18 @@
7
7
  // "Slet billede" (…) when nothing uses it (also not the site's own code, /editor/code-images.json). Its
8
8
  // name, under the title, can be changed with the pencil after it (not one the site's code uses),
9
9
  // saved with the tick: the file moves, the pages that use it get the new address in the same commit,
10
- // and the old address sends on to it (addresses.ts). "+" uploads a new file, saved at once. The same
10
+ // and the old address sends on to it (addresses.ts). The FAB uploads a new file, saved at once. The same
11
11
  // pictures (not the other files) are the gallery a picture is chosen from in the forms (useMediaPicker).
12
12
  // The buttons that run (add, rename, delete, upload new) show the loading indicator while they do.
13
13
  // The funnel in Filer also filters by a tag (fileTagFilter.mjs; only tags some file has).
14
14
  // The clock in the top bar, "Slettede filer", opens the files that were deleted (Deleted.tsx).
15
15
  import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
16
- import {
17
- ArrowsDownUp,
18
- CalendarBlank,
19
- Check,
20
- ClockCounterClockwise,
21
- PencilSimple,
22
- Plus,
23
- Trash,
24
- UploadSimple,
25
- X,
26
- } from '@phosphor-icons/react';
16
+ import { ArrowsDownUp, CalendarBlank, Check, ClockCounterClockwise, PencilSimple, Trash, UploadSimple, X } from '@phosphor-icons/react';
27
17
  import { moveRedirects, REDIRECTS } from './addresses';
28
18
  import { changesOf, flatten, readAll, REDIRECT_FILES } from './redirectData';
29
19
  import { ask } from './ask';
30
20
  import { dateName, headed, letterOf } from './Sheet';
21
+ import { CreateFab } from './CreateFab';
31
22
  import Screen from './Screen';
32
23
  import {
33
24
  Button,
@@ -229,8 +220,8 @@ function Gallery({
229
220
  // All the files, not only pictures (Filer), with their type.
230
221
  const all = !!used;
231
222
  // In Filer, kept for coming back to it (from a file's page); in a picker, from the start each time.
232
- const [kind, setKind] = useKept<'' | Kind>(all ? 'filer-type' : null, '');
233
- const [query, setQuery] = useKept(all ? 'filer-soeg' : null, '');
223
+ const [kind, setKind] = useKept<'' | Kind>(all ? 'files-type' : null, '');
224
+ const [query, setQuery] = useKept(all ? 'files-search' : null, '');
234
225
  const [sort, setSort] = useState<Sort>(() => {
235
226
  try {
236
227
  const saved = localStorage.getItem(SORT_KEY);
@@ -238,9 +229,9 @@ function Gallery({
238
229
  } catch {}
239
230
  return 'new';
240
231
  });
241
- const [date, setDate] = useKept(all ? 'filer-dato' : null, '');
242
- const [use, setUse] = useKept<'' | 'used' | 'unused'>(all ? 'filer-brug' : null, '');
243
- const [tag, setTag] = useKept(all ? 'filer-tag' : null, '');
232
+ const [date, setDate] = useKept(all ? 'files-date' : null, '');
233
+ const [use, setUse] = useKept<'' | 'used' | 'unused'>(all ? 'files-use' : null, '');
234
+ const [tag, setTag] = useKept(all ? 'files-tag' : null, '');
244
235
 
245
236
  function pickSort(next: Sort) {
246
237
  setSort(next);
@@ -419,7 +410,7 @@ const codeImages = () =>
419
410
  .then((r) => (r.ok ? (r.json() as Promise<string[]>) : null))
420
411
  .catch(() => null));
421
412
 
422
- // open: the address of a file to show picked at once (the route filer/<address>); ignored when no such file exists.
413
+ // open: the address of a file to show picked at once (the route files/<address>); ignored when no such file exists.
423
414
  export default function Media({ files, env, open }: { files: RepoFile[]; env: Env; open?: string }) {
424
415
  // The pictures and the other files.
425
416
  const pictures = [...mediaOf(files), ...filesOf(files)];
@@ -687,9 +678,8 @@ export default function Media({ files, env, open }: { files: RepoFile[]; env: En
687
678
  icon={ClockCounterClockwise}
688
679
  label={t.history.deletedOf(t.history.files)}
689
680
 
690
- onClick={() => (location.hash = 'slettet/filer')}
681
+ onClick={() => (location.hash = 'deleted/files')}
691
682
  />
692
- <IconButton icon={Plus} label={t.media.add} busy={adding} disabled={adding} onClick={add} />
693
683
  </>
694
684
  }
695
685
  >
@@ -727,6 +717,7 @@ export default function Media({ files, env, open }: { files: RepoFile[]; env: En
727
717
  <SheetTitle title={src ? fileName(src) : ''} onClose={() => dialog.current?.close()} />
728
718
  {!wide && src && <SheetBody>{details(src)}</SheetBody>}
729
719
  </BottomSheet>
720
+ <CreateFab label={t.media.add} busy={adding} disabled={adding} onClick={add} />
730
721
  </Screen>
731
722
  );
732
723
  }
package/src/MediaTags.tsx CHANGED
@@ -74,11 +74,11 @@ export default function MediaTags({ src, files, env }: { src: string; files: Rep
74
74
  keep="categories"
75
75
  footer={(close) => (
76
76
  <SheetLink
77
- href="#ny/kategorier"
77
+ href="#new/tags"
78
78
  onClick={(e) => {
79
79
  e.preventDefault();
80
80
  close();
81
- leaveTo('#ny/kategorier');
81
+ leaveTo('#new/tags');
82
82
  }}
83
83
  >
84
84
  {t.media.createTag}
@@ -1,4 +1,4 @@
1
- // "Notifikationer" (#notifikationer), which the bell in the top bar opens (Screen.tsx NotificationsLink): the
1
+ // "Notifikationer" (#notifications), which the bell in the top bar opens (Screen.tsx NotificationsLink): the
2
2
  // notifications of the editor and the apps (notificationData.ts), newest first, each with when it came and, when it had
3
3
  // one, a link to what it is about; an app's with the app's icon. Opening it marks them seen; at the top the bin clears
4
4
  // them and the sliders open "Konto", where each type is turned on or off.
@@ -41,7 +41,7 @@ export default function Notifications() {
41
41
  disabled={!items.length}
42
42
  onClick={clearNotifications}
43
43
  />
44
- <IconLink href="#konto" icon={SlidersHorizontal} label={t.library.notificationChoices} />
44
+ <IconLink href="#account" icon={SlidersHorizontal} label={t.library.notificationChoices} />
45
45
  </>
46
46
  }
47
47
  >
package/src/Parts.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Dele" › "Editor" (#udseende/dele/editor): the parts only the editor has (ui.tsx: the SEO rating, the
1
+ // "Udseende" › "Dele" › "Editor" (#appearance/parts/editor): the parts only the editor has (ui.tsx: the SEO rating, the
2
2
  // colour field, the choices of pages and icons, the form's layout …) in every state, on one screen, to see and agree on
3
3
  // how the editor looks, and below them the form fields (fields.tsx), one of each type with the settings it can have.
4
4
  // The parts the theme has too (buttons, fields, switches, lists, sheets …) are the theme's, shown under "UI-komponenter"
@@ -118,7 +118,7 @@ const GROUPS = (['buttons', 'fields', 'choices', 'lists', 'sheets', 'messages',
118
118
  }));
119
119
 
120
120
  export default function Parts({ env }: { env: Env }) {
121
- const [sort, setSort] = useState('nyeste');
121
+ const [sort, setSort] = useState('newest');
122
122
  const [filter, setFilter] = useState('');
123
123
  const [content, setContent] = useState('');
124
124
  const [picked, setPicked] = useState('a');
@@ -155,7 +155,7 @@ export default function Parts({ env }: { env: Env }) {
155
155
  const page = useMemo(() => exampleData().relations.pages[1].value, []);
156
156
 
157
157
  return (
158
- <Screen title={t.parts.screens.editor} back={{ href: '#udseende/dele', label: t.common.parts.editorParts }}>
158
+ <Screen title={t.parts.screens.editor} back={{ href: '#appearance/parts', label: t.common.parts.editorParts }}>
159
159
  <div className="space-y-10">
160
160
  <p className="text-muted">{t.parts.intro('packages/editor/src/ui.tsx')}</p>
161
161
  <PartsShown groups={GROUPS} keep="parts">
@@ -169,7 +169,7 @@ export default function Parts({ env }: { env: Env }) {
169
169
  </Part>
170
170
 
171
171
  <Part name="ArrowLink" use={use.arrowLink}>
172
- <ArrowLink href="#udseende/farver">{sample.editColors}</ArrowLink>
172
+ <ArrowLink href="#appearance/colors">{sample.editColors}</ArrowLink>
173
173
  </Part>
174
174
  </PartGroup>
175
175
 
@@ -199,7 +199,7 @@ export default function Parts({ env }: { env: Env }) {
199
199
  { value: 'top', label: sample.top },
200
200
  { value: 'list', label: sample.inList },
201
201
  ]}
202
- href="#udseende/dele/editor"
202
+ href="#appearance/parts/editor"
203
203
  onChange={setPlacement}
204
204
  />
205
205
  </div>
@@ -273,7 +273,7 @@ export default function Parts({ env }: { env: Env }) {
273
273
  max={3}
274
274
  footer={(close) => (
275
275
  <SheetLink
276
- href="#udseende/dele/editor"
276
+ href="#appearance/parts/editor"
277
277
  onClick={(e) => {
278
278
  e.preventDefault();
279
279
  close();
@@ -327,8 +327,8 @@ export default function Parts({ env }: { env: Env }) {
327
327
  value={sort}
328
328
  disabled={asMenu}
329
329
  options={[
330
- { value: 'nyeste', label: sample.newest },
331
- { value: 'aeldste', label: sample.oldest },
330
+ { value: 'newest', label: sample.newest },
331
+ { value: 'oldest', label: sample.oldest },
332
332
  ]}
333
333
  onChange={setSort}
334
334
  />
@@ -393,10 +393,10 @@ export default function Parts({ env }: { env: Env }) {
393
393
  { icon: FolderSimple, label: sample.media, count: 42 },
394
394
  { icon: PencilSimple, label: sample.pages, count: 12 },
395
395
  ].map((p) => (
396
- <ListItem key={p.label} href="#udseende/dele/editor" icon={p.icon} headline={p.label} count={p.count} arrow />
396
+ <ListItem key={p.label} href="#appearance/parts/editor" icon={p.icon} headline={p.label} count={p.count} arrow />
397
397
  ))}
398
398
  {/* With a text under its name: the icon beside the first two lines. */}
399
- <ListItem href="#udseende/dele/editor" icon={Gear} headline={sample.cookieBanner} supporting={sample.cookieHint} arrow />
399
+ <ListItem href="#appearance/parts/editor" icon={Gear} headline={sample.cookieBanner} supporting={sample.cookieHint} arrow />
400
400
  {/* With a picture before its name (the picture a page's colours came from). */}
401
401
  <ListItem
402
402
  leading={<LeadingImage src={examplePicture} />}
@@ -459,7 +459,7 @@ export default function Parts({ env }: { env: Env }) {
459
459
  <Part name="UnfoldButton, subListClass" use={use.unfold}>
460
460
  <ul className={listClass}>
461
461
  <ListItem
462
- href="#udseende/dele/editor"
462
+ href="#appearance/parts/editor"
463
463
  headline={sample.pages}
464
464
  actions={
465
465
  <span className="shrink-0 pr-1.5">
@@ -477,7 +477,7 @@ export default function Parts({ env }: { env: Env }) {
477
477
  <li className="pb-2">
478
478
  <ul className={subListClass}>
479
479
  {[1, 2, 3].map((n) => (
480
- <ListItem key={n} tone="sub" href="#udseende/dele/editor" headline={sample.subpage(n)} />
480
+ <ListItem key={n} tone="sub" href="#appearance/parts/editor" headline={sample.subpage(n)} />
481
481
  ))}
482
482
  </ul>
483
483
  </li>
@@ -534,8 +534,8 @@ export default function Parts({ env }: { env: Env }) {
534
534
  icon={ArrowsDownUp}
535
535
  value={sort}
536
536
  options={[
537
- { value: 'nyeste', label: sample.newest },
538
- { value: 'aeldste', label: sample.oldest },
537
+ { value: 'newest', label: sample.newest },
538
+ { value: 'oldest', label: sample.oldest },
539
539
  ]}
540
540
  onChange={setSort}
541
541
  />
@@ -572,8 +572,8 @@ export default function Parts({ env }: { env: Env }) {
572
572
  search={t.common.search}
573
573
  value={sort}
574
574
  options={[
575
- { value: 'nyeste', label: sample.newest },
576
- { value: 'aeldste', label: sample.oldest },
575
+ { value: 'newest', label: sample.newest },
576
+ { value: 'oldest', label: sample.oldest },
577
577
  ]}
578
578
  onChange={setSort}
579
579
  />
@@ -811,7 +811,7 @@ function fieldTypes(): FieldType[] {
811
811
  { name: 'phone', label: sample.phone, type: 'string', phone: '+45', digitsInPairs: 8 },
812
812
  { name: 'keyword', label: f.keyword, type: 'string', chips: 'one' },
813
813
  { name: 'keywords', label: f.keywords, type: 'string', chips: 'many' },
814
- { name: 'linkText', label: f.linkText, type: 'string', edit: { to: 'udseende/farver', text: f.editColors } },
814
+ { name: 'linkText', label: f.linkText, type: 'string', edit: { to: 'appearance/colors', text: f.editColors } },
815
815
  ],
816
816
  },
817
817
  {
package/src/Redirects.tsx CHANGED
@@ -1,28 +1,28 @@
1
- // "Videresendelser" under Indhold (#seo/videresendelser): two rows with how many
1
+ // "Videresendelser" under Indhold (#seo/redirects): two rows with how many
2
2
  // each has, each opening a screen of its own.
3
- // "Alle videresendelser" (#seo/videresendelser/alle): every address of a page the site sends on elsewhere
3
+ // "Alle videresendelser" (#seo/redirects/all): every address of a page the site sends on elsewhere
4
4
  // (redirectData.ts), by address, with where it sends to, and search. Tapping one opens a sheet where
5
- // where it sends to can be changed, or the redirect deleted; "+" adds a new one (in the editor's own,
5
+ // where it sends to can be changed, or the redirect deleted; the FAB adds a new one (in the editor's own,
6
6
  // src/data/redirects.json). Each save is one commit, and none sends on through another afterwards
7
7
  // (flatten): a redirect to an address that is sent on goes straight to where that ends, and those
8
8
  // that went on through a deleted one go on to where it sent them.
9
- // "Ikke fundet" (#seo/videresendelser/ikke-fundet): the addresses visitors came to that do not exist (notFound.ts), most visits
9
+ // "Ikke fundet" (#seo/redirects/not-found): the addresses visitors came to that do not exist (notFound.ts), most visits
10
10
  // first. Tapping one opens the same sheet, to send it on to a page (then it is off that list) or take
11
11
  // it off the list. One that has become a page or sends on since is not shown. Over each list, as over
12
12
  // the lists under "Indhold", the search, the order (kept in this browser; by address under the first
13
13
  // letter) and, over the redirects, the funnel that shows only those to one page or post, picked as a
14
14
  // chip. The Save and Delete buttons show the loading indicator while their save runs (Forget does not).
15
15
  import { useEffect, useMemo, useRef, useState } from 'react';
16
- import { ArrowRight, ArrowsDownUp, Funnel, LinkBreak, Plus, Signpost, Trash, X } from '@phosphor-icons/react';
16
+ import { ArrowRight, ArrowsDownUp, Funnel, LinkBreak, Signpost, Trash, X } from '@phosphor-icons/react';
17
17
  import { addressOf, escape, REDIRECTS } from './addresses';
18
18
  import { ask } from './ask';
19
+ import { CreateFab } from './CreateFab';
19
20
  import Screen from './Screen';
20
21
  import {
21
22
  Button,
22
23
  InputChip,
23
24
  Choice,
24
25
  Group,
25
- IconButton,
26
26
  listClass,
27
27
  SupportingText,
28
28
  SearchRow,
@@ -66,15 +66,15 @@ type Editing = { from: string; to: string; isNew: boolean; missing?: Missing };
66
66
  export type Tab = 'redirects' | 'missing';
67
67
 
68
68
  // Where each list opens, under "Videresendelser".
69
- export const TAB_ROUTES: Record<Tab, string> = { redirects: 'seo/videresendelser/alle', missing: 'seo/videresendelser/ikke-fundet' };
69
+ export const TAB_ROUTES: Record<Tab, string> = { redirects: 'seo/redirects/all', missing: 'seo/redirects/not-found' };
70
70
  /** Where "Videresendelser" opens, under "Søgemaskineoptimering". */
71
- export const REDIRECTS_ROUTE = 'seo/videresendelser';
71
+ export const REDIRECTS_ROUTE = 'seo/redirects';
72
72
 
73
73
  // The orders of each list: the redirects by their address or by the page they send to, the addresses
74
74
  // that do not exist by the visits, the last visit or the address. Kept in this browser, each its own.
75
75
  type Sort = 'az' | 'za' | 'page' | 'visits' | 'last';
76
76
  const SORTS: Record<Tab, Sort[]> = { redirects: ['az', 'za', 'page'], missing: ['visits', 'last', 'az', 'za'] };
77
- const sortKey = (tab: Tab) => `editor-sort-${tab === 'redirects' ? 'videresendelser' : 'ikke-fundet'}`;
77
+ const sortKey = (tab: Tab) => `editor-sort-${tab === 'redirects' ? 'redirects' : 'not-found'}`;
78
78
 
79
79
  function savedSort(tab: Tab): Sort {
80
80
  try {
@@ -99,7 +99,7 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
99
99
  }, []);
100
100
  const all = useMemo(() => (files ? readAll(files) : null), [files]);
101
101
  const entries = useMemo(() => (all ? entriesOf(all) : []), [all]);
102
- const [query, setQuery] = useKept('videresendelser-soeg', '');
102
+ const [query, setQuery] = useKept('redirects-search', '');
103
103
  const q = query.trim().toLowerCase();
104
104
  const name = (to: string) => whatIsAt(to, env.relations);
105
105
  // The pages and posts the redirects send to (not a picture or another site), by their names as in the
@@ -255,7 +255,7 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
255
255
  { tab: 'missing' as const, label: t.redirects.missing.title, icon: LinkBreak, count: missing && files ? stillMissing.length : null },
256
256
  ];
257
257
  return (
258
- <Screen title={t.common.parts.redirects} wide back={backFrom('videresendelser')}>
258
+ <Screen title={t.common.parts.redirects} wide back={backFrom('redirects')}>
259
259
  {problem && <ProblemNote>{problem}</ProblemNote>}
260
260
  <Group>
261
261
  {rows.map((r) => (
@@ -279,17 +279,6 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
279
279
  ? t.redirects.count(entries.length)
280
280
  : undefined
281
281
  }
282
- actions={
283
- tab === 'redirects' && (
284
- <IconButton
285
- icon={Plus}
286
- label={t.redirects.new}
287
-
288
- disabled={!files}
289
- onClick={() => open({ from: '', to: '', isNew: true })}
290
- />
291
- )
292
- }
293
282
  >
294
283
  <p className="mb-4 text-muted">{tab === 'redirects' ? t.redirects.intro : t.redirects.missing.intro}</p>
295
284
  <SearchRow
@@ -449,6 +438,9 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
449
438
  </SheetBody>
450
439
  )}
451
440
  </BottomSheet>
441
+ {tab === 'redirects' && (
442
+ <CreateFab label={t.redirects.new} disabled={!files} onClick={() => open({ from: '', to: '', isNew: true })} />
443
+ )}
452
444
  </Screen>
453
445
  );
454
446
  }