@morphemecms/cms 0.12.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 (121) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/defaults/blockTexts.ts +9 -9
  3. package/defaults/collections.ts +257 -257
  4. package/defaults/colors.ts +74 -74
  5. package/defaults/company.ts +9 -9
  6. package/defaults/editor.ts +65 -65
  7. package/defaults/i18n/da-DK/blockTexts.ts +12 -0
  8. package/defaults/i18n/da-DK/collections.ts +220 -0
  9. package/defaults/i18n/da-DK/colors.ts +102 -0
  10. package/defaults/i18n/da-DK/company.ts +7 -0
  11. package/defaults/i18n/da-DK/editor.ts +13 -0
  12. package/defaults/i18n/da-DK/index.ts +31 -0
  13. package/defaults/i18n/da-DK/internalLinks.ts +12 -0
  14. package/defaults/i18n/da-DK/opens.ts +7 -0
  15. package/defaults/i18n/da-DK/popup.ts +9 -0
  16. package/defaults/i18n/da-DK/privacy.ts +38 -0
  17. package/defaults/i18n/da-DK/rules.ts +56 -0
  18. package/defaults/i18n/da-DK/sitedata.ts +8 -0
  19. package/defaults/i18n/da-DK/standard.ts +8 -0
  20. package/defaults/i18n/da-DK/standardPages.ts +17 -0
  21. package/defaults/internalLinks.ts +8 -8
  22. package/defaults/opens.ts +2 -2
  23. package/defaults/popup.tsx +7 -7
  24. package/defaults/privacy.ts +37 -37
  25. package/defaults/rules.ts +68 -66
  26. package/defaults/schema.ts +1 -1
  27. package/defaults/sitedata.ts +4 -4
  28. package/defaults/standard.ts +9 -6
  29. package/defaults/standardPages.ts +14 -14
  30. package/defaults/texts.ts +7 -510
  31. package/lib/appRoute.d.mts +2 -2
  32. package/lib/appRoute.mjs +3 -3
  33. package/lib/appRoute.test.mjs +8 -8
  34. package/lib/licenses.mjs +2 -2
  35. package/lib/licenses.owner.test.mjs +9 -0
  36. package/lib/menu.mjs +4 -4
  37. package/lib/menu.test.mjs +101 -101
  38. package/lib/scheduled.mjs +1 -1
  39. package/lib/storeEntries.test.mjs +3 -0
  40. package/lib/translate.d.mts +11 -0
  41. package/lib/translate.mjs +13 -1
  42. package/lib/translate.test.mjs +23 -2
  43. package/package.json +1 -1
  44. package/scripts/app-screenshots.mjs +7 -7
  45. package/scripts/check-content.mjs +10 -1
  46. package/scripts/check-language.mjs +0 -1
  47. package/scripts/editor-screens.mjs +3 -3
  48. package/site/defineSite.mjs +2 -2
  49. package/site/schema.ts +3 -3
  50. package/src/Advanced.tsx +2 -2
  51. package/src/App.tsx +78 -77
  52. package/src/AppAdds.tsx +1 -1
  53. package/src/AppList.tsx +15 -15
  54. package/src/BlockDialog.tsx +1 -1
  55. package/src/BlockParts.tsx +6 -6
  56. package/src/BlockSettings.tsx +2 -2
  57. package/src/BrandForm.tsx +2 -2
  58. package/src/Colors.tsx +6 -6
  59. package/src/CompanyForm.tsx +1 -1
  60. package/src/Consents.tsx +3 -3
  61. package/src/DataForm.tsx +3 -3
  62. package/src/Deleted.tsx +3 -3
  63. package/src/EditorFooter.tsx +2 -2
  64. package/src/EntryForm.tsx +2 -2
  65. package/src/FooterSettings.tsx +2 -2
  66. package/src/HeaderSettings.tsx +3 -3
  67. package/src/Keywords.tsx +5 -5
  68. package/src/Library.tsx +71 -71
  69. package/src/Licenses.tsx +8 -8
  70. package/src/MainDrawerParts.tsx +3 -3
  71. package/src/MainMenu.tsx +1 -1
  72. package/src/Media.tsx +7 -7
  73. package/src/MediaTags.tsx +2 -2
  74. package/src/Notifications.tsx +2 -2
  75. package/src/Parts.tsx +17 -17
  76. package/src/Redirects.tsx +8 -8
  77. package/src/Screen.tsx +3 -3
  78. package/src/SeoScores.tsx +6 -6
  79. package/src/SideColumn.tsx +2 -2
  80. package/src/StartFab.tsx +2 -2
  81. package/src/ThemeStore.tsx +4 -4
  82. package/src/Tokens.tsx +2 -2
  83. package/src/apps.ts +5 -5
  84. package/src/checkSite.ts +86 -20
  85. package/src/editor.css +4 -16
  86. package/src/examples.ts +1 -1
  87. package/src/fabScreens.test.mjs +4 -4
  88. package/src/fabScreens.ts +4 -4
  89. package/src/i18n/da-DK/library.ts +2 -2
  90. package/src/i18n/da-DK/text.ts +1 -1
  91. package/src/i18n/en-US/apps.ts +1 -1
  92. package/src/i18n/en-US/common.ts +8 -8
  93. package/src/i18n/en-US/library.ts +5 -5
  94. package/src/i18n/en-US/parts.ts +3 -3
  95. package/src/i18n/en-US/text.ts +1 -1
  96. package/src/i18n/index.ts +4 -8
  97. package/src/images.ts +2 -2
  98. package/src/licenseData.ts +3 -3
  99. package/src/menuChildren.ts +5 -5
  100. package/src/movedRoutes.d.mts +8 -1
  101. package/src/movedRoutes.mjs +100 -19
  102. package/src/movedRoutes.test.mjs +123 -6
  103. package/src/nav.ts +1 -1
  104. package/src/notificationData.ts +1 -1
  105. package/src/schema.ts +2 -2
  106. package/src/site.ts +37 -13
  107. package/src/standard/accordion/index.tsx +1 -1
  108. package/src/standard/accordion/shared.ts +1 -1
  109. package/src/standard/buttons/index.tsx +1 -1
  110. package/src/standard/cards/index.tsx +1 -1
  111. package/src/standard/cards/shared.ts +1 -1
  112. package/src/standard/cookies/index.tsx +1 -1
  113. package/src/standard/posts/index.tsx +1 -1
  114. package/src/standard/testimonials/index.tsx +1 -1
  115. package/src/standard/testimonials/list.ts +1 -1
  116. package/src/store.ts +4 -4
  117. package/src/tagUses.d.mts +1 -1
  118. package/src/tagUses.mjs +2 -2
  119. package/src/tagUses.test.mjs +1 -1
  120. package/src/ui.tsx +7 -7
  121. package/theme/layouts/Base.astro +2 -2
package/src/editor.css CHANGED
@@ -329,7 +329,7 @@
329
329
  /* The main drawer's header row (MainDrawerMenuButton in ui.tsx): the account row is a pill, shaped and shaded as a drawer
330
330
  item is when pointed at. Its inline padding puts the avatar's centre over the items' icons (the avatar is wider than the
331
331
  24px icon: half the difference less than the items' padding). The avatar has a 1px border inside its size.
332
- 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. */
333
333
  .editor-main-drawer-header {
334
334
  --list-item-padding-block: 0.375rem;
335
335
  --list-item-two-line-min-height: var(--navigation-drawer-item-height, 3.5rem);
@@ -878,9 +878,7 @@ html {
878
878
 
879
879
  /* A text with blocks (a page, a fixed page's sections): the cards close together, with the free line
880
880
  between two of them to write in: thin, until the cursor is in it (editor-gap-here, MarkdownEditor.tsx)
881
- 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
882
- line break share the cell, so the line is as tall as the hint, however many lines it wraps to, and the cursor sits
883
- at the start of the hint. */
881
+ or the pointer over it, when it opens up and says so. */
884
882
  .editor-spaced .site-block {
885
883
  margin-block: 0.25rem;
886
884
  }
@@ -901,37 +899,27 @@ html {
901
899
  }
902
900
 
903
901
  .editor-spaced .ProseMirror:focus > p.editor-gap-here:has(> br:only-child) {
904
- display: grid;
905
902
  line-height: 1.5rem;
906
903
  }
907
904
 
908
905
  .editor-spaced .ProseMirror:focus > p.editor-gap-here:has(> br:only-child)::before {
909
906
  content: var(--editor-gap-text);
910
- grid-area: 1 / 1;
907
+ position: absolute;
911
908
  color: var(--muted);
912
909
  pointer-events: none;
913
910
  }
914
911
 
915
- .editor-spaced .ProseMirror:focus > p.editor-gap-here > br:only-child {
916
- grid-area: 1 / 1;
917
- }
918
-
919
912
  @media (hover: hover) {
920
913
  .editor-spaced .ProseMirror > p:has(> br:only-child):hover {
921
- display: grid;
922
914
  line-height: 1.5rem;
923
915
  }
924
916
 
925
917
  .editor-spaced .ProseMirror > p:has(> br:only-child):hover::before {
926
918
  content: var(--editor-gap-text);
927
- grid-area: 1 / 1;
919
+ position: absolute;
928
920
  color: var(--muted);
929
921
  pointer-events: none;
930
922
  }
931
-
932
- .editor-spaced .ProseMirror > p:hover > br:only-child {
933
- grid-area: 1 / 1;
934
- }
935
923
  }
936
924
 
937
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.
@@ -11,14 +11,14 @@ test('the start screen makes a page or a blog post', () => {
11
11
  });
12
12
 
13
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: '#ny/pages' }]);
15
- assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard]), [{ label: 'New promotion', href: '#ny/popups' }]);
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
16
  });
17
17
 
18
18
  test('a list of several kinds has one action per kind', () => {
19
19
  assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard, mailchimp]), [
20
- { label: 'Standard', href: '#ny/popups/standard' },
21
- { label: 'Mailchimp', href: '#ny/popups/mailchimp' },
20
+ { label: 'Standard', href: '#new/popups/standard' },
21
+ { label: 'Mailchimp', href: '#new/popups/mailchimp' },
22
22
  ]);
23
23
  });
24
24
 
package/src/fabScreens.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  /** The collections a new entry can be made in from the start screen, in this order: pages and blog posts. */
5
5
  export const START_FAB = ['pages', 'blog'];
6
6
 
7
- /** A list's create actions, as a label and where it goes: with several kinds one per kind (`#ny/<id>/<kind>`), else one
8
- * named `create` (`#ny/<id>`); none for a list that cannot make entries (no `create`). */
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
9
  export function listFab(
10
10
  collection: { id: string; create?: string },
11
11
  kinds: { value: string; label: string }[],
12
12
  ): { label: string; href: string }[] {
13
13
  if (!collection.create) return [];
14
- if (kinds.length > 1) return kinds.map((k) => ({ label: k.label, href: `#ny/${collection.id}/${k.value}` }));
15
- return [{ label: collection.create, href: `#ny/${collection.id}` }];
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
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}`;
@@ -20,8 +20,8 @@ export type ThirdParty =
20
20
  /** The licence of one's own work that grants nothing ("all rights reserved"), as npm writes it. */
21
21
  export const UNLICENSED = 'UNLICENSED';
22
22
 
23
- /** Who brings a work: the editor, the site's own code, or an app or a theme (`app/<id>`, `tema/<id>`, LicenseOwner.id). */
24
- export type LicenseWhere = 'editor' | 'site' | `app/${string}` | `tema/${string}`;
23
+ /** Who brings a work: the editor, the site's own code, or an app or a theme (`app/<id>`, `theme/<id>`, LicenseOwner.id). */
24
+ export type LicenseWhere = 'editor' | 'site' | `app/${string}` | `theme/${string}`;
25
25
 
26
26
  /** A third-party work as the build found it: its id (a package's name; `ikoner/<id>` for an icon set; `<owner>/<name>`
27
27
  * for another work an app or a theme declares), its name, version, who made it, its licence's name and text (`standard`:
@@ -46,7 +46,7 @@ export type LicensedWork = {
46
46
  /** An app or a theme with its own licence (`text`: LICENSE in its folder, none when it is UNLICENSED) and the works it
47
47
  * brings (their ids: those it declares, and those shipped through them alone), and whether the site is built with it. */
48
48
  export type LicenseOwner = {
49
- id: `app/${string}` | `tema/${string}`;
49
+ id: `app/${string}` | `theme/${string}`;
50
50
  kind: 'app' | 'theme';
51
51
  /** Its folder's name in src/apps/ or src/themes/. */
52
52
  key: string;
@@ -18,16 +18,16 @@ const listOf = (ids: string[]): MenuChild[] =>
18
18
 
19
19
  /** The sub-items of a menu item, in their order: none for most. */
20
20
  export function childrenOf(id: string, files: RepoFile[]): MenuChild[] {
21
- if (id === 'lister') return listOf(listsShown(files));
22
- if (id === 'virksomheden') return companyParts.map((p) => ({ id: p.id, label: p.label, href: `#virksomheden/${p.id}` }));
21
+ if (id === 'lists') return listOf(listsShown(files));
22
+ if (id === 'company') return companyParts.map((p) => ({ id: p.id, label: p.label, href: `#company/${p.id}` }));
23
23
  if (id === 'apps') {
24
24
  const installed = installedApps(files);
25
25
  return apps.filter((a) => installed.includes(a.id)).map((a) => ({ id: a.id, label: a.label, href: `#apps/${a.id}` }));
26
26
  }
27
- if (id === 'videresendelser') {
27
+ if (id === 'redirects') {
28
28
  return [
29
- { id: 'alle', label: t.redirects.all, href: `#${TAB_ROUTES.redirects}` },
30
- { id: 'ikke-fundet', label: t.redirects.missing.title, href: `#${TAB_ROUTES.missing}` },
29
+ { id: 'all', label: t.redirects.all, href: `#${TAB_ROUTES.redirects}` },
30
+ { id: 'not-found', label: t.redirects.missing.title, href: `#${TAB_ROUTES.missing}` },
31
31
  ];
32
32
  }
33
33
  return [];
@@ -1,3 +1,10 @@
1
1
  /** Where each moved screen is now, by its old address. */
2
2
  export const MOVED: Record<string, string>;
3
- export function movedRoute(route: string, more?: Record<string, string>): string;
3
+ /**
4
+ * Old route prefix (Danish, one or more segments, as the routes were before the ids got English names) -> its new
5
+ * English prefix. Only a route that starts with the old prefix is renamed, and the rest of it is data (a file path, a
6
+ * slug, an id) that keeps its name. A key segment `*` matches any one segment and keeps it (in the value too).
7
+ * Old links keep working through it. The longest matching key wins.
8
+ */
9
+ export const RENAMED: Record<string, string>;
10
+ export function movedRoute(route: string, more?: Record<string, string>, renamed?: Record<string, string>): string;
@@ -1,35 +1,116 @@
1
- // The editor's screens that have moved, by their old address after the "#": a link or a bookmark to one opens it where
2
- // it is now. Exposes MOVED and movedRoute (pure). Used by App.tsx (routeOf), which adds the app store's own.
1
+ // The editor's screens that have moved or been renamed, by their old address after the "#": a link or a bookmark to one
2
+ // opens it where it is now. Exposes MOVED, RENAMED and movedRoute (pure). Used by App.tsx (routeOf), which adds the app store's own.
3
3
 
4
4
  /** Where each moved screen is now, by its old address. */
5
5
  export const MOVED = /** @type {Record<string, string>} */ ({
6
- farver: 'udseende/farver',
7
- 'virksomheden/logo': 'udseende/logo',
8
- 'tema/blokvalg': 'blokvalg',
9
- dele: 'udseende/dele',
10
- tema: 'udseende',
11
- 'tema/farver': 'udseende/farver',
12
- 'tema/logo': 'udseende/logo',
13
- 'tema/dele': 'udseende/dele',
14
- 'tema/dele/editor': 'udseende/dele/editor',
15
- 'virksomheden/bank': 'virksomheden/betaling/bank',
16
- videresendelser: 'seo/videresendelser',
17
- 'videresendelser/alle': 'seo/videresendelser/alle',
18
- 'videresendelser/ikke-fundet': 'seo/videresendelser/ikke-fundet',
6
+ farver: 'appearance/colors',
7
+ 'virksomheden/logo': 'appearance/logo',
8
+ 'tema/blokvalg': 'block-settings',
9
+ dele: 'appearance/parts',
10
+ tema: 'appearance',
11
+ 'tema/farver': 'appearance/colors',
12
+ 'tema/logo': 'appearance/logo',
13
+ 'tema/dele': 'appearance/parts',
14
+ 'tema/dele/editor': 'appearance/parts/editor',
15
+ 'virksomheden/bank': 'company/payment/bank',
16
+ videresendelser: 'seo/redirects',
17
+ 'videresendelser/alle': 'seo/redirects/all',
18
+ 'videresendelser/ikke-fundet': 'seo/redirects/not-found',
19
19
  // The settings of the booking and newsletter apps were on a screen "Promovering"; they are on the apps' own pages now.
20
20
  promovering: 'apps',
21
21
  'promovering/calendly': 'apps/app_a030dfba69793996',
22
22
  'promovering/mailchimp': 'apps/app_5c4b6ba067ef127f',
23
23
  // The header's menus and settings were on a screen of their own under "Globalt"; they are under "Udseende" now.
24
- header: 'udseende/header',
24
+ header: 'appearance/header',
25
25
  // The footer's form was the file's own screen; it is under "Udseende" now.
26
- 'src/data/footer.json': 'udseende/footer',
26
+ 'src/data/footer.json': 'appearance/footer',
27
27
  });
28
28
 
29
29
  /**
30
- * The address a screen is at now: where it moved to (`more` first, then MOVED), else the same address.
30
+ * Old route prefix (Danish, one or more segments, as the routes were before the ids got English names) -> its new
31
+ * English prefix. Only a route that starts with the old prefix is renamed, and the rest of it is data (a file path, a
32
+ * slug, an id) that keeps its name. A key segment `*` matches any one segment and keeps it (in the value too).
33
+ * Old links keep working through it. The longest matching key wins.
34
+ */
35
+ export const RENAMED = /** @type {Record<string, string>} */ ({
36
+ indstillinger: 'settings',
37
+ 'indstillinger/appstore/temaer': 'settings/appstore/themes',
38
+ 'apps/*/indstillinger': 'apps/*/settings',
39
+ 'apps/*/om': 'apps/*/about',
40
+ 'apps/udvikler': 'apps/developer',
41
+ udseende: 'appearance',
42
+ 'udseende/temaer': 'appearance/themes',
43
+ 'udseende/ikonpakker': 'appearance/icon-packs',
44
+ 'udseende/bevaegelse': 'appearance/motion',
45
+ 'udseende/sidekolonne': 'appearance/side-column',
46
+ 'udseende/farver': 'appearance/colors',
47
+ 'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
48
+ 'udseende/dele': 'appearance/parts',
49
+ 'udseende/dele/blokke': 'appearance/parts/blocks',
50
+ 'udseende/dele/liste': 'appearance/parts/list',
51
+ 'udseende/dele/vaerdier': 'appearance/parts/values',
52
+ 'udseende/standardsider': 'appearance/default-pages',
53
+ maaling: 'analytics',
54
+ privatliv: 'privacy',
55
+ 'privatliv/cookiebanner': 'privacy/cookie-banner',
56
+ 'privatliv/samtykker': 'privacy/consents',
57
+ 'privatliv/privatlivssider': 'privacy/privacy-pages',
58
+ lister: 'lists',
59
+ liste: 'list',
60
+ 'liste/kategorier': 'list/tags',
61
+ 'liste/udtalelser': 'list/testimonials',
62
+ 'liste/mobilmenu': 'list/mobile-menu',
63
+ ny: 'new',
64
+ 'ny/kategorier': 'new/tags',
65
+ 'ny/udtalelser': 'new/testimonials',
66
+ slettet: 'deleted',
67
+ 'slettet/kategorier': 'deleted/tags',
68
+ 'slettet/udtalelser': 'deleted/testimonials',
69
+ 'slettet/mobilmenu': 'deleted/mobile-menu',
70
+ 'slettet/filer': 'deleted/files',
71
+ virksomheden: 'company',
72
+ 'virksomheden/kontakt': 'company/contact',
73
+ 'virksomheden/sociale-medier': 'company/social-media',
74
+ 'virksomheden/betaling': 'company/payment',
75
+ blokke: 'blocks',
76
+ filer: 'files',
77
+ 'seo/videresendelser': 'seo/redirects',
78
+ 'seo/videresendelser/alle': 'seo/redirects/all',
79
+ 'seo/videresendelser/ikke-fundet': 'seo/redirects/not-found',
80
+ 'seo/interne-links': 'seo/internal-links',
81
+ 'seo/vurdering': 'seo/rating',
82
+ blokvalg: 'block-settings',
83
+ avanceret: 'advanced',
84
+ licenser: 'licenses',
85
+ konto: 'account',
86
+ notifikationer: 'notifications',
87
+ });
88
+
89
+ /**
90
+ * The address a screen is at now: where it moved to (`more` first, then MOVED), else the address with its longest old
91
+ * prefix in `renamed` replaced and the rest as it is, else the same address.
31
92
  * @param {string} route
32
93
  * @param {Record<string, string>} [more]
94
+ * @param {Record<string, string>} [renamed]
33
95
  * @returns {string}
34
96
  */
35
- export const movedRoute = (route, more = {}) => more[route] ?? MOVED[route] ?? route;
97
+ export const movedRoute = (route, more = {}, renamed = RENAMED) => {
98
+ const moved = more[route] ?? MOVED[route];
99
+ if (moved !== undefined) return moved;
100
+ const parts = route.split('/');
101
+ let best = null;
102
+ for (const key of Object.keys(renamed)) {
103
+ const old = key.split('/');
104
+ if (old.length > parts.length || (best && old.length <= best.old.length)) continue;
105
+ if (old.every((segment, n) => segment === '*' || segment === parts[n])) best = { key, old };
106
+ }
107
+ if (!best) return route;
108
+ const kept = parts.slice(0, best.old.length);
109
+ let n = 0;
110
+ const now = renamed[best.key].split('/').map((segment) => {
111
+ if (segment !== '*') return segment;
112
+ while (best.old[n] !== '*') n += 1;
113
+ return kept[n++];
114
+ });
115
+ return [...now, ...parts.slice(best.old.length)].join('/');
116
+ };