@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
package/src/editor.css CHANGED
@@ -4,6 +4,10 @@
4
4
  @reference '../theme/styles/base.css';
5
5
 
6
6
  :where(:root) {
7
+ /* The menus and the toolbar of the text: Material 3's menu surface and shadow (elevation level 2). */
8
+ --editor-menu-surface: var(--surface-container-lowest);
9
+ --editor-menu-shadow: var(--elevation-2);
10
+
7
11
  /* How a check went (CheckList: the SEO rating), as PageSpeed shows it: the mark before a check that is fine and one
8
12
  that can be done better, and the score's ring (from 90 good, from 50 average, else bad) with its number and the
9
13
  light circle in it; the same in every theme. */
@@ -33,13 +37,14 @@
33
37
  --editor-section-rhythm: 0.625em;
34
38
 
35
39
  /* A form's "on this page" area (SectionPane in ui.tsx) against the window's end edge: its width (the card is as wide as the navigation drawer), the card in it
36
- stands --section-pane-gap in from the end edge and from under the top bar. */
40
+ stands --section-pane-gap in from the end edge and from the window's top edge. */
37
41
  --section-pane-width: calc(var(--navigation-drawer-width, 22.5rem) + var(--section-pane-gap));
38
42
  --section-pane-gap: 2rem;
39
43
 
40
- /* The side panel of a block's fields and the section settings (BottomSheet `panel` in ui.tsx) on large and wider: half the
41
- space beside the start panel, the content column taking the other half (see below). */
42
- --side-panel-width: calc((100vw - var(--column-start)) / 2);
44
+ /* The side panel of a block's fields and the section settings (BlockDialog's standard side sheet from expanded, BottomSheet
45
+ `panel` in ui.tsx from large): M3's side sheet is 400dp (25rem) wide at most; where the window is narrower it is half
46
+ the space beside the start panel, the content column taking the other half (see below). */
47
+ --side-panel-width: min(25rem, calc((100vw - var(--column-start)) / 2));
43
48
  }
44
49
 
45
50
  /* In dark mode the score's number in the light colours, which are read on the dark, and Google's dark colours. */
@@ -324,7 +329,7 @@
324
329
  /* The main drawer's header row (MainDrawerMenuButton in ui.tsx): the account row is a pill, shaped and shaded as a drawer
325
330
  item is when pointed at. Its inline padding puts the avatar's centre over the items' icons (the avatar is wider than the
326
331
  24px icon: half the difference less than the items' padding). The avatar has a 1px border inside its size.
327
- The row is as high as a drawer item (56px). */
332
+ The pill is as high as a drawer item (56px); the row around it is the top bar's height. */
328
333
  .editor-main-drawer-header {
329
334
  --list-item-padding-block: 0.375rem;
330
335
  --list-item-two-line-min-height: var(--navigation-drawer-item-height, 3.5rem);
@@ -403,10 +408,25 @@
403
408
  z-index: 30;
404
409
  }
405
410
 
411
+ /* A FAB whose action runs or cannot be used now (FabAction.busy, .disabled): dimmed, and nothing to press. */
412
+ .editor-fab :is(.extended-fab, .fab):disabled {
413
+ opacity: 0.38;
414
+ pointer-events: none;
415
+ }
416
+
406
417
  :root:has(.editor-fab) .snackbar-stack {
407
418
  bottom: calc(6rem + env(safe-area-inset-bottom));
408
419
  }
409
420
 
421
+ /* While a form's save bar is up (FormFrame.tsx) the FAB stands above it, so it does not cover Save; the messages above both. */
422
+ :root:has(.editor-save-bar) .editor-fab {
423
+ bottom: calc(6.5rem + env(safe-area-inset-bottom));
424
+ }
425
+
426
+ :root:has(.editor-save-bar):has(.editor-fab) .snackbar-stack {
427
+ bottom: calc(11.5rem + env(safe-area-inset-bottom));
428
+ }
429
+
410
430
  /* From medium the navigation rail stands at the start edge of the window (MainMenu.tsx), so the messages are in the middle of
411
431
  the content next to it and the save bar starts where it ends; at expanded and wider, while the standard drawer is open
412
432
  in the rail's place, they keep clear of the drawer instead. The widths are the theme's. The top bar and its tabs (Screen.tsx)
@@ -461,8 +481,8 @@
461
481
  not less than nothing and not more than the space left between the panels less the column. Only when that space is
462
482
  smaller than the column does it get narrower. (Measured against the window's width, 100vw, scrollbar and all.) Below
463
483
  medium nothing stands beside the content and the column is centred by auto margins. While the side panel is open (see
464
- the end of this block) the panel takes the end half of the space beside the start panel and the column the other half:
465
- --column-width stays the column's most, so on a very wide window it is narrower than its half. */
484
+ the end of this block) the column is centred in the space between the start panel and the side panel by auto margins
485
+ (the content's end padding keeps it clear of the panel), still no wider than --column-width. */
466
486
  :root {
467
487
  --column-start: 0px;
468
488
  --column-end: 0px;
@@ -504,25 +524,26 @@
504
524
  right: var(--section-pane-width);
505
525
  }
506
526
 
507
- /* BottomSheet's `panel` (.editor-side-panel): the theme's modal side sheet at the window's end edge under the top bar,
508
- the full remaining height; its backdrop is clear. */
527
+ /* BottomSheet's `panel` (.editor-side-panel): the theme's modal side sheet at the window's end edge, the full window
528
+ height like the standard one; its backdrop is clear. */
509
529
  .editor-side-panel {
510
- inset: var(--top-bar-height) 0 0 auto;
530
+ inset: 0 0 0 auto;
511
531
  width: var(--side-panel-width);
512
- height: calc(100dvh - var(--top-bar-height));
532
+ height: 100dvh;
513
533
  }
514
534
  .editor-side-panel:dir(rtl) {
515
- inset: var(--top-bar-height) auto 0 0;
535
+ inset: 0 auto 0 0;
516
536
  }
517
537
  .editor-side-panel::backdrop {
518
538
  background: transparent;
519
539
  }
520
540
  }
521
541
 
522
- /* The block dialog's standard side sheet (.editor-side-sheet: BlockDialog.tsx, not modal, from medium) stands at the window's
523
- end edge under the top bar, the full remaining height. BlockDialog.tsx sets [data-side-panel] on the root while it is
524
- open: it takes the "on this page" area's place (that is hidden), and the content, which stays in use, keeps clear of it. */
525
- @media (width >= theme(--breakpoint-medium)) {
542
+ /* The block dialog's standard side sheet (.editor-side-sheet: BlockDialog.tsx, not modal, from expanded) stands at the window's
543
+ end edge from the top of the window to its bottom (M3), with its divider the whole height. BlockDialog.tsx sets
544
+ [data-side-panel] on the root while it is open: the top bar (.editor-top-bar-slot, topBarSlot.tsx) ends at the sheet's
545
+ start edge, it takes the "on this page" area's place (that is hidden), and the content, which stays in use, keeps clear of it. */
546
+ @media (width >= theme(--breakpoint-expanded)) {
526
547
  :root[data-side-panel] {
527
548
  --column-end: var(--side-panel-width);
528
549
  }
@@ -532,19 +553,25 @@
532
553
  :root[data-side-panel] .editor-content {
533
554
  padding-inline-end: var(--side-panel-width);
534
555
  }
556
+ :root[data-side-panel] .editor-column {
557
+ margin-inline: auto;
558
+ }
535
559
  :root[data-side-panel] .editor-save-bar {
536
560
  right: var(--side-panel-width);
537
561
  }
538
562
  .editor-side-sheet {
539
563
  position: fixed;
540
- inset: var(--top-bar-height) 0 0 auto;
564
+ inset: 0 0 0 auto;
541
565
  z-index: 20;
542
566
  width: var(--side-panel-width);
543
567
  max-width: none;
544
- height: calc(100dvh - var(--top-bar-height));
568
+ height: 100dvh;
545
569
  }
546
570
  .editor-side-sheet:dir(rtl) {
547
- inset: var(--top-bar-height) auto 0 0;
571
+ inset: 0 auto 0 0;
572
+ }
573
+ :root[data-side-panel] .editor-top-bar-slot {
574
+ padding-inline-end: var(--side-panel-width);
548
575
  }
549
576
  }
550
577
 
@@ -632,9 +659,9 @@ html {
632
659
  gap: 0.125rem;
633
660
  border: 1px solid var(--line);
634
661
  border-radius: 9999px;
635
- background: var(--surface);
662
+ background: var(--editor-menu-surface);
636
663
  padding: 0.25rem;
637
- box-shadow: var(--shadow);
664
+ box-shadow: var(--editor-menu-shadow);
638
665
  }
639
666
 
640
667
  .editor-text .site-block-menu button {
@@ -656,6 +683,12 @@ html {
656
683
  color: var(--accent);
657
684
  }
658
685
 
686
+ /* The text's toolbar (MarkdownEditor.tsx, Toolbar in ui.tsx): the menu surface and shadow. */
687
+ .editor-tools {
688
+ --toolbar-standard-surface: var(--editor-menu-surface);
689
+ box-shadow: var(--editor-menu-shadow);
690
+ }
691
+
659
692
  /* The icons over the selected text, on a phone at the bottom over the keyboard (selectionMenu in
660
693
  MarkdownEditor.tsx). */
661
694
  .editor-selection-menu {
@@ -665,9 +698,9 @@ html {
665
698
  gap: 0.125rem;
666
699
  border: 1px solid var(--line);
667
700
  border-radius: 9999px;
668
- background: var(--surface);
701
+ background: var(--editor-menu-surface);
669
702
  padding: 0.25rem;
670
- box-shadow: var(--shadow);
703
+ box-shadow: var(--editor-menu-shadow);
671
704
  }
672
705
 
673
706
  .editor-selection-menu[hidden],
@@ -845,9 +878,7 @@ html {
845
878
 
846
879
  /* A text with blocks (a page, a fixed page's sections): the cards close together, with the free line
847
880
  between two of them to write in: thin, until the cursor is in it (editor-gap-here, MarkdownEditor.tsx)
848
- or the pointer over it, when it opens up and says so. An open line is a one-cell grid: the hint (::before) and the
849
- line break share the cell, so the line is as tall as the hint, however many lines it wraps to, and the cursor sits
850
- at the start of the hint. */
881
+ or the pointer over it, when it opens up and says so. */
851
882
  .editor-spaced .site-block {
852
883
  margin-block: 0.25rem;
853
884
  }
@@ -868,37 +899,27 @@ html {
868
899
  }
869
900
 
870
901
  .editor-spaced .ProseMirror:focus > p.editor-gap-here:has(> br:only-child) {
871
- display: grid;
872
902
  line-height: 1.5rem;
873
903
  }
874
904
 
875
905
  .editor-spaced .ProseMirror:focus > p.editor-gap-here:has(> br:only-child)::before {
876
906
  content: var(--editor-gap-text);
877
- grid-area: 1 / 1;
907
+ position: absolute;
878
908
  color: var(--muted);
879
909
  pointer-events: none;
880
910
  }
881
911
 
882
- .editor-spaced .ProseMirror:focus > p.editor-gap-here > br:only-child {
883
- grid-area: 1 / 1;
884
- }
885
-
886
912
  @media (hover: hover) {
887
913
  .editor-spaced .ProseMirror > p:has(> br:only-child):hover {
888
- display: grid;
889
914
  line-height: 1.5rem;
890
915
  }
891
916
 
892
917
  .editor-spaced .ProseMirror > p:has(> br:only-child):hover::before {
893
918
  content: var(--editor-gap-text);
894
- grid-area: 1 / 1;
919
+ position: absolute;
895
920
  color: var(--muted);
896
921
  pointer-events: none;
897
922
  }
898
-
899
- .editor-spaced .ProseMirror > p:hover > br:only-child {
900
- grid-area: 1 / 1;
901
- }
902
923
  }
903
924
 
904
925
  /* While a sheet or a dialog is open (shown modally, showModal), the page under it does not scroll (only the sheet does).
package/src/examples.ts CHANGED
@@ -1,4 +1,4 @@
1
- // What "Dele" (#udseende/dele: Parts.tsx, BlockParts.tsx) shows the parts and blocks with: made-up pages, posts, tags,
1
+ // What "Dele" (#appearance/parts: Parts.tsx, BlockParts.tsx) shows the parts and blocks with: made-up pages, posts, tags,
2
2
  // testimonials, the lists' made-up rows (`examples` of a list), cookies and contact details in the editor's language,
3
3
  // the apps' made-up settings (AppSettings.example), and a plain picture, never what the site has, so the parts look the
4
4
  // same on every site.
@@ -0,0 +1,28 @@
1
+ // Tests for src/fabScreens.ts: the start screen's FAB menu and the create actions of a list. Run: node --test src/fabScreens.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { test } from 'node:test';
4
+ import { START_FAB, listFab } from './fabScreens.ts';
5
+
6
+ const standard = { value: 'standard', label: 'Standard' };
7
+ const mailchimp = { value: 'mailchimp', label: 'Mailchimp' };
8
+
9
+ test('the start screen makes a page or a blog post', () => {
10
+ assert.deepEqual(START_FAB, ['pages', 'blog']);
11
+ });
12
+
13
+ test('a list without kinds, or with one, has one action named after its create label', () => {
14
+ assert.deepEqual(listFab({ id: 'pages', create: 'New page' }, []), [{ label: 'New page', href: '#new/pages' }]);
15
+ assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard]), [{ label: 'New promotion', href: '#new/popups' }]);
16
+ });
17
+
18
+ test('a list of several kinds has one action per kind', () => {
19
+ assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard, mailchimp]), [
20
+ { label: 'Standard', href: '#new/popups/standard' },
21
+ { label: 'Mailchimp', href: '#new/popups/mailchimp' },
22
+ ]);
23
+ });
24
+
25
+ test('a list that cannot make entries has no action', () => {
26
+ assert.deepEqual(listFab({ id: 'orders' }, []), []);
27
+ assert.deepEqual(listFab({ id: 'orders' }, [standard, mailchimp]), []);
28
+ });
@@ -0,0 +1,16 @@
1
+ // What the create FAB offers: the start screen's menu (START_FAB, the collection ids) and a list's own (listFab: one
2
+ // action, or one per kind). Pure; the icons and the FAB itself are added by StartFab.tsx and ListFab.tsx.
3
+
4
+ /** The collections a new entry can be made in from the start screen, in this order: pages and blog posts. */
5
+ export const START_FAB = ['pages', 'blog'];
6
+
7
+ /** A list's create actions, as a label and where it goes: with several kinds one per kind (`#new/<id>/<kind>`), else one
8
+ * named `create` (`#new/<id>`); none for a list that cannot make entries (no `create`). */
9
+ export function listFab(
10
+ collection: { id: string; create?: string },
11
+ kinds: { value: string; label: string }[],
12
+ ): { label: string; href: string }[] {
13
+ if (!collection.create) return [];
14
+ if (kinds.length > 1) return kinds.map((k) => ({ label: k.label, href: `#new/${collection.id}/${k.value}` }));
15
+ return [{ label: collection.create, href: `#new/${collection.id}` }];
16
+ }
@@ -106,8 +106,8 @@ export const library: Messages['library'] = {
106
106
  statusShown: { all: 'Alle', published: 'Udgivet', draft: 'Kladde', unlisted: 'Skjult', scheduled: 'Planlagt' },
107
107
  sort: 'Sortér',
108
108
  sorts: {
109
- nyeste: 'Nyeste først',
110
- aeldste: 'Ældste først',
109
+ newest: 'Nyeste først',
110
+ oldest: 'Ældste først',
111
111
  az: 'A–Å',
112
112
  za: 'Å–A',
113
113
  savedNewest: 'Senest ændret først',
@@ -35,5 +35,5 @@ export const text: Messages['text'] = {
35
35
  hiddenOnPage: 'Skjult på siden',
36
36
  showOnPage: 'Vis på siden',
37
37
  hideOnPage: 'Skjul på siden',
38
- typeHere: 'Begynd at skrive… Gør din første overskrift god, for den bliver den H1, som læsere og søgemaskiner ser først.',
38
+ typeHere: 'Skriv tekst her …',
39
39
  };
@@ -33,7 +33,7 @@ export const apps = {
33
33
  cookiesAsk: 'Set only once the visitor has said yes in the cookie banner',
34
34
  noCookies: 'Sets no cookies',
35
35
  developer: 'Developer',
36
- /** On a developer's page (#apps/udvikler/<navn>): over their apps, and the number of them by their row on an app's page. */
36
+ /** On a developer's page (#apps/developer/<name>): over their apps, and the number of them by their row on an app's page. */
37
37
  developerApps: 'Apps',
38
38
  appCount: (n: number) => (n === 1 ? '1 app' : `${n} apps`),
39
39
  /** At the address of a developer no app has. */
@@ -107,23 +107,23 @@ export const common = {
107
107
  lists: 'Lists',
108
108
  blocks: 'Block templates',
109
109
  media: 'Files',
110
- /** How the site looks (#udseende, Library.tsx Theme): the theme, the colours, the logo and icon, and the site's own
110
+ /** How the site looks (#appearance, Library.tsx Theme): the theme, the colours, the logo and icon, and the site's own
111
111
  * parts there (appearance in packages/editor/defaults/editor.ts: "Standardsider"). */
112
112
  appearance: 'Appearance',
113
- /** The themes installed (#udseende/temaer, Library.tsx Themes), the first row under "Udseende". */
113
+ /** The themes installed (#appearance/themes, Library.tsx Themes), the first row under "Udseende". */
114
114
  themes: 'Themes',
115
- /** The icon packs installed, to choose the one the site draws its icons with (#udseende/ikonpakker, IconPacks). */
115
+ /** The icon packs installed, to choose the one the site draws its icons with (#appearance/icon-packs, IconPacks). */
116
116
  iconPacks: 'Icon packs',
117
- /** How the site moves, Material 3's motion schemes (#udseende/bevaegelse, MotionSchemes in Library.tsx). */
117
+ /** How the site moves, Material 3's motion schemes (#appearance/motion, MotionSchemes in Library.tsx). */
118
118
  motion: 'Motion',
119
- /** The block drawn in a column beside the text of the pages (#udseende/sidekolonne, SideColumn.tsx). */
119
+ /** The block drawn in a column beside the text of the pages (#appearance/side-column, SideColumn.tsx). */
120
120
  sideColumn: 'Side column',
121
121
  colors: 'Colors',
122
122
  brand: 'Logo and icon',
123
123
  blockSettings: 'Block settings',
124
124
  /** The apps that come with the site (AppList.tsx, #apps). */
125
125
  apps: 'Apps',
126
- /** The parts the editor and the site are built of (#udseende/dele: Parts.tsx, BlockParts.tsx). */
126
+ /** The parts the editor and the site are built of (#appearance/parts: Parts.tsx, BlockParts.tsx). */
127
127
  editorParts: 'Parts',
128
128
  /** Under Indstillinger: the redirects and the site's own parts (#seo, Library.tsx Seo). */
129
129
  seo: 'Search engine optimization',
@@ -131,9 +131,9 @@ export const common = {
131
131
  account: 'Account',
132
132
  /** The last section of the main drawer, and the first part of the path to a setting (placeOf in site.ts). */
133
133
  settings: 'Settings',
134
- /** Under Indstillinger: the faster editor (#avanceret, Advanced.tsx). */
134
+ /** Under Indstillinger: the faster editor (#advanced, Advanced.tsx). */
135
135
  advanced: 'Advanced',
136
- /** Under Indstillinger: the licences of what the site and the editor are made with (#licenser, Licenses.tsx). */
136
+ /** Under Indstillinger: the licences of what the site and the editor are made with (#licenses, Licenses.tsx). */
137
137
  licenses: 'Licenses',
138
138
  /** Under Indstillinger: the CMS's version and its updates (#morpheme). */
139
139
  morpheme: 'Morpheme',
@@ -10,7 +10,7 @@ export const library = {
10
10
  createNew: 'Create',
11
11
  githubAccount: (login: string) => `GitHub account: ${login}`,
12
12
  logOut: 'Log out',
13
- /** The bell in the top bar and its screen (#notifikationer, Notifications.tsx): the messages shown at the bottom. */
13
+ /** The bell in the top bar and its screen (#notifications, Notifications.tsx): the messages shown at the bottom. */
14
14
  notifications: 'Notifications',
15
15
  /** The bell's name while some are not seen yet. */
16
16
  notificationsNew: (count: number) => `Notifications, ${count} new`,
@@ -144,8 +144,8 @@ export const library = {
144
144
  statusShown: { all: 'All', published: 'Published', draft: 'Draft', unlisted: 'Hidden', scheduled: 'Scheduled' },
145
145
  sort: 'Sort',
146
146
  sorts: {
147
- nyeste: 'Newest first',
148
- aeldste: 'Oldest first',
147
+ newest: 'Newest first',
148
+ oldest: 'Oldest first',
149
149
  az: 'A–Z',
150
150
  za: 'Z–A',
151
151
  /** The pages, by when they were last saved. */
@@ -153,7 +153,7 @@ export const library = {
153
153
  savedOldest: 'Least recently changed first',
154
154
  /** A list of kinds (Promovering), under a heading for each kind. */
155
155
  type: 'By type',
156
- /** "Standardblokke" (#blokke), in the groups by where the blocks come from. */
156
+ /** "Standardblokke" (#blocks), in the groups by where the blocks come from. */
157
157
  group: 'By group',
158
158
  },
159
159
  /** The funnel over a list of what is shown on pages (Promovering), with its filters in one sheet. */
@@ -172,7 +172,7 @@ export const library = {
172
172
  /** The filter by whether an entry is turned on, and its first choice. */
173
173
  onOffFilter: 'Status',
174
174
  allOnOff: 'All',
175
- /** The filters over "Standardblokke" (#blokke): by the group (where the block comes from) and by whether
175
+ /** The filters over "Standardblokke" (#blocks): by the group (where the block comes from) and by whether
176
176
  * the block is on a page, each with its first choice. */
177
177
  groupFilter: 'Group',
178
178
  allGroups: 'All groups',
@@ -1,4 +1,4 @@
1
- // "Dele" (#udseende/dele): the editor's parts (Parts.tsx), what each is for and the texts in the examples, and the standard
1
+ // "Dele" (#appearance/parts): the editor's parts (Parts.tsx), what each is for and the texts in the examples, and the standard
2
2
  // blocks and the apps' (BlockParts.tsx), all with the made-up site of examples.ts.
3
3
  export const parts = {
4
4
  intro: (file: string) =>
@@ -253,7 +253,7 @@ export const parts = {
253
253
  /** What a kind of entry an app adds (a pop-up) is drawn with. */
254
254
  entry: { name: 'An example', heading: 'A heading', text: 'A short text that tells a little more.' },
255
255
  },
256
- /** "Dele" › "Værdier" (#udseende/dele/vaerdier, Tokens.tsx): the theme's design tokens, each with what it is used for. */
256
+ /** "Dele" › "Værdier" (#appearance/parts/values, Tokens.tsx): the theme's design tokens, each with what it is used for. */
257
257
  tokens: {
258
258
  intro: 'The theme’s values for corners, shadows, typography, and colors, as the theme sets them.',
259
259
  groups: { shape: 'Corners', elevation: 'Shadows', type: 'Typography', color: 'Colors' },
@@ -355,7 +355,7 @@ export const parts = {
355
355
  /** A token the theme does not set, with the one it gets instead. */
356
356
  from: (token: string, value: string, from: string) => `${token}: ${value} (as ${from})`,
357
357
  },
358
- /** "Dele" (#udseende/dele) and its screens: the editor's parts, the theme's UI components, the blocks (the standard
358
+ /** "Dele" (#appearance/parts) and its screens: the editor's parts, the theme's UI components, the blocks (the standard
359
359
  * ones and the apps') and the lists of kinds ("Promotion"). */
360
360
  screens: {
361
361
  editor: 'Editor',
@@ -43,5 +43,5 @@ export const text = {
43
43
  showOnPage: 'Show on the page',
44
44
  hideOnPage: 'Hide on the page',
45
45
  /** Shown in an empty line of the text, under the pointer or the cursor. */
46
- typeHere: 'Start typing… Make your first heading count, because it becomes the H1 that readers and search engines see first.',
46
+ typeHere: 'Write text here…',
47
47
  };
package/src/i18n/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  // (siteText, @site/texts). The blocks' drawings and the commits (siteT) stay in the site's
10
10
  // language, so the site looks as it is, and the history is in one language, whoever saved.
11
11
  // A value the editor writes into content (a default text) is in the site's language too (inSiteLanguage).
12
+ // The standard parts are written in English; their Danish is in defaults/i18n/da-DK/, the default of @site/texts.
12
13
  // A language is a folder with the texts by the part of the editor they are in (en-US/index.ts). English is the
13
14
  // reference: its texts are written first (Messages is its type), and every other language translates them; a new
14
15
  // one is a copy of da-DK with its texts translated from en-US, added to `messages`. TypeScript says when a
@@ -82,7 +83,7 @@ export const t: Messages = messages[locale];
82
83
  export const siteT: Messages = messages[siteLocale];
83
84
 
84
85
  /** The language the package's standard parts (defaults/) are written in, whatever the site's language is. */
85
- export const DEFAULTS_LOCALE: Locale = 'da-DK';
86
+ export const DEFAULTS_LOCALE: Locale = 'en-US';
86
87
 
87
88
  /** Whether the site's own texts (written in the site's language) or the package's standard parts (written in
88
89
  * DEFAULTS_LOCALE) may be shown in another language than they are written in. */
@@ -100,9 +101,7 @@ export const appTexts: Record<string, Texts> = Object.fromEntries(
100
101
  }),
101
102
  );
102
103
 
103
- const own: Record<string, string> = translatingSite
104
- ? Object.assign({}, ...Object.values(appTexts).map((a) => a[locale]), (texts as Texts)[locale])
105
- : {};
104
+ const own = Object.assign({}, ...Object.values(appTexts).map((a) => a[locale]), (texts as Texts)[locale]);
106
105
 
107
106
  /** One of the site's own texts (src/site/, src/themes/<theme>/blocks/, src/apps/), written in the site's language, in the editor's:
108
107
  * its translation in @site/texts or in a block's texts.ts, else as it is written. `values` fill in
@@ -111,10 +110,7 @@ export function siteText(text: string, values?: Record<string, string | number>)
111
110
  return translate(own, text, values);
112
111
  }
113
112
 
114
- const inSite: Record<string, string> =
115
- siteLocale === DEFAULTS_LOCALE
116
- ? {}
117
- : Object.assign({}, ...Object.values(appTexts).map((a) => a[siteLocale]), (texts as Texts)[siteLocale]);
113
+ const inSite = Object.assign({}, ...Object.values(appTexts).map((a) => a[siteLocale]), (texts as Texts)[siteLocale]);
118
114
 
119
115
  /** A text of the package's standard parts (written in DEFAULTS_LOCALE) in the site's language, for a value that the
120
116
  * editor writes into content (a default button text): its translation in @site/texts or in a block's texts.ts, else
package/src/images.ts CHANGED
@@ -98,7 +98,7 @@ export async function prepareImage(file: File): Promise<Upload> {
98
98
  const now = new Date();
99
99
  const folder = `${config.folders.media}/${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}`;
100
100
  // A short random ending, so two pictures called IMG_1234 do not get the same name.
101
- const name = `${slugify(file.name.replace(/\.[^.]*$/, '')) || 'billede'}-${Math.random().toString(36).slice(2, 6)}`;
101
+ const name = `${slugify(file.name.replace(/\.[^.]*$/, '')) || 'picture'}-${Math.random().toString(36).slice(2, 6)}`;
102
102
  const ext = blob.type === 'image/webp' ? 'webp' : 'jpg';
103
103
  const upload = {
104
104
  url: URL.createObjectURL(blob),
@@ -119,7 +119,7 @@ export const FILE_MAX_MB = 10;
119
119
  export async function prepareFile(file: File, taken: string[]): Promise<Upload> {
120
120
  if (file.size > FILE_MAX_MB * 1024 * 1024) throw new Error(t.media.tooBig(FILE_MAX_MB));
121
121
  const folder = config.folders.files;
122
- const stem = slugify(file.name.replace(/\.[^.]*$/, '')) || 'fil';
122
+ const stem = slugify(file.name.replace(/\.[^.]*$/, '')) || 'file';
123
123
  const ending = pickedEnding(file);
124
124
  let path = `${folder}/${stem}.${ending}`;
125
125
  for (let n = 2; taken.includes(path); n++) path = `${folder}/${stem}-${n}.${ending}`;
@@ -0,0 +1,137 @@
1
+ // Tests for src/legacyMove.ts: when a legacy store copy counts as installed, and the one list of changes that moves it
2
+ // to its permanent folder. Run: node --test src/legacyMove.test.mjs
3
+ import assert from 'node:assert/strict';
4
+ import { test } from 'node:test';
5
+ import { appliedTo, installedHere, legacyOf, mergeChanges, moveChanges } from './legacyMove.ts';
6
+
7
+ const PATHS = {
8
+ appsFolder: 'src/apps',
9
+ themesFolder: 'src/themes',
10
+ installed: 'src/data/apps.json',
11
+ settings: 'src/data/apps',
12
+ theme: 'src/data/theme.json',
13
+ icons: 'src/data/icons.json',
14
+ blockSettings: 'src/data/block-settings.json',
15
+ popups: 'src/data/popups',
16
+ cookieList: 'src/data/cookie-list',
17
+ };
18
+ const VENUS = { id: 'theme_fa395ad84690acbd', legacy: 'venus' };
19
+ const MOBILEPAY = { id: 'app_ae0b5e17636668bc', legacy: 'mobilepay' };
20
+ const file = (path, text, oid = `oid-${path}`) => ({ path, oid, text });
21
+ const json = (x) => `${JSON.stringify(x, null, 2)}\n`;
22
+ const next = { files: [{ path: 'theme.json', content: '{}' }], record: { version: '2.0.0', ref: '2.0.0', files: [] } };
23
+ const find = (changes, path) => changes.find((c) => c.path === path);
24
+
25
+ test('a legacy folder with a store.json is installed', () => {
26
+ assert.equal(legacyOf(VENUS, [{ id: 'venus', store: { version: '1.2.0' } }]), 'venus');
27
+ });
28
+
29
+ test('a legacy folder without a store.json is the site own', () => {
30
+ assert.equal(legacyOf(VENUS, [{ id: 'venus' }]), null);
31
+ assert.equal(legacyOf(VENUS, []), null);
32
+ assert.equal(legacyOf({ id: VENUS.id }, [{ id: 'venus', store: { version: '1.2.0' } }]), null);
33
+ });
34
+
35
+ test('moving a theme writes the new folder, deletes the old files and rewrites theme.json and icons.json', () => {
36
+ const old = [file('src/themes/venus/theme.json', '{"a":1}'), file('src/themes/venus/store.json', '{}')];
37
+ const site = [file(PATHS.theme, json({ theme: 'venus' })), file(PATHS.icons, json({ pack: 'venus/x' }))];
38
+ const changes = moveChanges(VENUS, 'venus', next, old, site, PATHS);
39
+ assert.equal(find(changes, `src/themes/${VENUS.id}/theme.json`).content, '{}');
40
+ assert.ok(find(changes, `src/themes/${VENUS.id}/store.json`));
41
+ for (const f of old) assert.deepEqual(find(changes, f.path), { path: f.path, content: null, oid: f.oid });
42
+ assert.deepEqual(JSON.parse(find(changes, PATHS.theme).content), { theme: VENUS.id });
43
+ assert.deepEqual(JSON.parse(find(changes, PATHS.icons).content), { pack: `${VENUS.id}/x` });
44
+ });
45
+
46
+ test('a system icon pack and another theme stay as they are', () => {
47
+ const site = [file(PATHS.theme, json({ theme: 'saturn' })), file(PATHS.icons, json({ pack: 'phosphor' }))];
48
+ const changes = moveChanges(VENUS, 'venus', next, [], site, PATHS);
49
+ assert.equal(find(changes, PATHS.theme), undefined);
50
+ assert.equal(find(changes, PATHS.icons), undefined);
51
+ });
52
+
53
+ test('moving an app renames its settings file and keeps the sorted list', () => {
54
+ const settings = '{ "key": "kept as it is" }\n';
55
+ const site = [
56
+ file(PATHS.installed, json({ installed: ['calendly', 'mobilepay', 'zeta'] })),
57
+ file('src/data/apps/mobilepay.json', settings),
58
+ ];
59
+ const changes = moveChanges(MOBILEPAY, 'mobilepay', { files: [], record: next.record }, [], site, PATHS);
60
+ assert.deepEqual(find(changes, 'src/data/apps/mobilepay.json'), {
61
+ path: 'src/data/apps/mobilepay.json',
62
+ content: null,
63
+ oid: 'oid-src/data/apps/mobilepay.json',
64
+ });
65
+ assert.deepEqual(find(changes, `src/data/apps/${MOBILEPAY.id}.json`), {
66
+ path: `src/data/apps/${MOBILEPAY.id}.json`,
67
+ content: settings,
68
+ oid: null,
69
+ });
70
+ assert.deepEqual(JSON.parse(find(changes, PATHS.installed).content).installed, ['calendly', MOBILEPAY.id, 'zeta'].sort());
71
+ });
72
+
73
+ test('moving an app rewrites block-settings, popups and the cookie list, and only the legacy values', () => {
74
+ const site = [
75
+ file(PATHS.blockSettings, json({ off: ['popups/mobilepay', 'popups/other', 'text', 'x/mobilepay-2'] })),
76
+ file('src/data/popups/a.json', json({ type: 'mobilepay', title: 'A' })),
77
+ file('src/data/popups/b.json', json({ type: 'other' })),
78
+ file('src/data/cookie-list/c.json', json({ name: 'mobilepay', app: 'mobilepay' })),
79
+ file('src/data/cookie-list/d.json', json({ app: 'other' })),
80
+ ];
81
+ const changes = moveChanges(MOBILEPAY, 'mobilepay', { files: [], record: next.record }, [], site, PATHS);
82
+ assert.deepEqual(JSON.parse(find(changes, PATHS.blockSettings).content).off, [
83
+ `popups/${MOBILEPAY.id}`,
84
+ 'popups/other',
85
+ 'text',
86
+ 'x/mobilepay-2',
87
+ ]);
88
+ assert.equal(JSON.parse(find(changes, 'src/data/popups/a.json').content).type, MOBILEPAY.id);
89
+ assert.equal(JSON.parse(find(changes, 'src/data/popups/a.json').content).title, 'A');
90
+ assert.equal(find(changes, 'src/data/popups/b.json'), undefined);
91
+ const cookie = JSON.parse(find(changes, 'src/data/cookie-list/c.json').content);
92
+ assert.deepEqual(cookie, { name: 'mobilepay', app: MOBILEPAY.id });
93
+ assert.equal(find(changes, 'src/data/cookie-list/d.json'), undefined);
94
+ assert.equal(find(changes, PATHS.theme), undefined);
95
+ });
96
+
97
+ test('an item without a legacy gets the normal update and no delete', () => {
98
+ const old = [file(`src/themes/${VENUS.id}/old.json`, '{}'), file(`src/themes/${VENUS.id}/theme.json`, '{}')];
99
+ const changes = moveChanges({ id: VENUS.id }, null, next, old, [file(PATHS.theme, json({ theme: 'venus' }))], PATHS);
100
+ assert.deepEqual(find(changes, `src/themes/${VENUS.id}/old.json`), {
101
+ path: `src/themes/${VENUS.id}/old.json`,
102
+ content: null,
103
+ oid: 'oid-src/themes/theme_fa395ad84690acbd/old.json',
104
+ });
105
+ assert.equal(find(changes, PATHS.theme), undefined);
106
+ assert.equal(changes.filter((c) => c.content === null).length, 1);
107
+ });
108
+
109
+ test('a move with a release that adds a cookie changes each cookie-list file once', () => {
110
+ const old = file('src/data/cookie-list/a.json', json({ name: 'a', app: 'mobilepay' }), 'oid-a');
111
+ const site = [old];
112
+ const moved = moveChanges(MOBILEPAY, 'mobilepay', { files: [], record: next.record }, [], site, PATHS);
113
+ // What the editor's cookieChanges does for the new release on the site after the move: the new cookie is a new file.
114
+ const after = appliedTo(site, moved);
115
+ assert.equal(JSON.parse(after[0].text).app, MOBILEPAY.id);
116
+ const cookies = [
117
+ { path: 'src/data/cookie-list/b.json', content: json({ name: 'b', app: MOBILEPAY.id }), oid: null },
118
+ { path: 'src/data/cookie-list/a.json', content: null, oid: after[0].oid },
119
+ ];
120
+ const all = mergeChanges(moved, cookies);
121
+ assert.equal(new Set(all.map((c) => c.path)).size, all.length);
122
+ assert.deepEqual(find(all, 'src/data/cookie-list/a.json'), { path: 'src/data/cookie-list/a.json', content: null, oid: 'oid-a' });
123
+ assert.equal(JSON.parse(find(all, 'src/data/cookie-list/b.json').content).app, MOBILEPAY.id);
124
+ // A cookie that stays: only the rewrite, once, under the new id.
125
+ const kept = mergeChanges(moved, []);
126
+ assert.equal(kept.filter((c) => c.path === old.path).length, 1);
127
+ assert.equal(JSON.parse(find(kept, old.path).content).app, MOBILEPAY.id);
128
+ });
129
+
130
+ test('installedHere: counts a new id, or a legacy copy from the store, as installed', () => {
131
+ const entry = { id: 'app_ae0b5e17636668bc', legacy: 'mobilepay' };
132
+ assert.equal(installedHere(entry, ['app_ae0b5e17636668bc'], []), true);
133
+ assert.equal(installedHere(entry, ['mobilepay'], [{ id: 'mobilepay', store: {} }]), true);
134
+ assert.equal(installedHere(entry, ['mobilepay'], [{ id: 'mobilepay' }]), false);
135
+ assert.equal(installedHere(entry, [], [{ id: 'mobilepay', store: {} }]), false);
136
+ assert.equal(installedHere({ id: 'app_1' }, ['other'], [{ id: 'other', store: {} }]), false);
137
+ });