@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/App.tsx CHANGED
@@ -1,50 +1,50 @@
1
1
  // The editor at /editor/ (docs/editor.md): sign in with GitHub, pick a part of the site, edit and
2
2
  // save. The layout: the full-height navigation rail or drawer (MainMenu.tsx) with, beside it, the top bar (topBarSlot.tsx) over the screen. The address after # says what is open, so reloading keeps it:
3
3
  // (empty) the start page
4
- // liste/<id> the list of a part, e.g. liste/priser, and liste/<id>/<app> a tab an installed app adds to it
5
- // (AppDefinition.tabs): liste/cookies/cookiescanner
6
- // lister the short lists (the testimonials, the apps' lists)
7
- // indstillinger/appstore the app store, its apps, and indstillinger/appstore/temaer its themes (AppList.tsx StorePage); the
4
+ // list/<id> the list of a part, e.g. list/priser, and list/<id>/<app> a tab an installed app adds to it
5
+ // (AppDefinition.tabs): list/cookies/cookiescanner
6
+ // lists the short lists (the testimonials, the apps' lists)
7
+ // settings/appstore the app store, its apps, and settings/appstore/themes its themes (AppList.tsx StorePage); the
8
8
  // other settings are screens of their own, in the drawer's last section (MainMenu.tsx)
9
- // ny/<id> a new entry, ny/<id>/<kind> one of a kind (a pop-up: standard, or an app's)
10
- // virksomheden the business, and virksomheden/<part> its parts: kontakt, betaling; and
11
- // virksomheden/betaling/<part> its own parts: mobilepay, bank
9
+ // new/<id> a new entry, new/<id>/<kind> one of a kind (a pop-up: standard, or an app's)
10
+ // company the business, and company/<part> its parts: contact, payment; and
11
+ // company/payment/<part> its own parts: mobilepay, bank
12
12
  // <screen> a screen with parts of the site's own (partScreens), and <screen>/<part> its parts:
13
- // privatliv/cookiebanner, privatliv/privatlivssider, or one of the editor's own screens (Part.opens):
14
- // privatliv/samtykker the visitors' answers in the cookie banner (Consents.tsx); and the settings of the apps installed with
15
- // its category: maaling/google-analytics
16
- // filer the uploaded pictures, PDFs, videos and zip files (Media.tsx)
17
- // seo "Søgemaskineoptimering" (Library.tsx Seo): seo/videresendelser the redirects' two lists
18
- // (Redirects.tsx): seo/videresendelser/alle the redirects, and seo/videresendelser/ikke-fundet
19
- // the addresses visitors came to that do not exist; seo/vurdering the SEO rating of every page and
13
+ // privacy/cookie-banner, privacy/privacy-pages, or one of the editor's own screens (Part.opens):
14
+ // privacy/consents the visitors' answers in the cookie banner (Consents.tsx); and the settings of the apps installed with
15
+ // its category: analytics/google-analytics
16
+ // files the uploaded pictures, PDFs, videos and zip files (Media.tsx)
17
+ // seo "Søgemaskineoptimering" (Library.tsx Seo): seo/redirects the redirects' two lists
18
+ // (Redirects.tsx): seo/redirects/all the redirects, and seo/redirects/not-found
19
+ // the addresses visitors came to that do not exist; seo/rating the SEO rating of every page and
20
20
  // post (SeoScores.tsx); and seo/<part> the site's own (seoParts)
21
- // udseende how the site looks (Library.tsx Theme): udseende/temaer the themes installed (Themes) and udseende/temaer/<theme>
22
- // a theme's page, with the button that swaps to it (ThemePage), udseende/ikonpakker the icon packs (IconPacks),
23
- // udseende/bevaegelse how it moves (MotionSchemes), udseende/sidekolonne the block beside the pages' text (SideColumn),
24
- // udseende/header the header's menus and settings with its preview (HeaderSettings.tsx; once at header),
25
- // udseende/footer the footer's texts and links with its preview (FooterSettings.tsx; once at src/data/footer.json),
26
- // udseende/farver the colours (Colors.tsx;
27
- // udseende/farver/reservefarver the site's own while an app changes the pages'), udseende/logo the logo
28
- // and icon (BrandForm.tsx), udseende/dele the parts (Library.tsx PartsHome): udseende/dele/editor the editor's, every
29
- // one in every state (Parts.tsx), udseende/dele/ui the theme's UI components, udseende/dele/blokke the
30
- // blocks, the standard ones and the apps', and udseende/dele/liste/<list> each kind of a list of kinds
31
- // ("Promovering"), its own and the apps' (BlockParts.tsx), and udseende/dele/vaerdier the
21
+ // appearance how the site looks (Library.tsx Theme): appearance/themes the themes installed (Themes) and appearance/themes/<theme>
22
+ // a theme's page, with the button that swaps to it (ThemePage), appearance/icon-packs the icon packs (IconPacks),
23
+ // appearance/motion how it moves (MotionSchemes), appearance/side-column the block beside the pages' text (SideColumn),
24
+ // appearance/header the header's menus and settings with its preview (HeaderSettings.tsx; once at header),
25
+ // appearance/footer the footer's texts and links with its preview (FooterSettings.tsx; once at src/data/footer.json),
26
+ // appearance/colors the colours (Colors.tsx;
27
+ // appearance/colors/fallback-colors the site's own while an app changes the pages'), appearance/logo the logo
28
+ // and icon (BrandForm.tsx), appearance/parts the parts (Library.tsx PartsHome): appearance/parts/editor the editor's, every
29
+ // one in every state (Parts.tsx), appearance/parts/ui the theme's UI components, appearance/parts/blocks the
30
+ // blocks, the standard ones and the apps', and appearance/parts/list/<list> each kind of a list of kinds
31
+ // ("Promovering"), its own and the apps' (BlockParts.tsx), and appearance/parts/values the
32
32
  // theme's design tokens, "Værdier" (Tokens.tsx)
33
- // blokvalg the blocks that can be put in, "Blokindstillinger" (BlockSettings.tsx)
34
- // avanceret "Avanceret", under "Indstillinger": the copy of the site's files in R2 (Advanced.tsx)
33
+ // block-settings the blocks that can be put in, "Blokindstillinger" (BlockSettings.tsx)
34
+ // advanced "Avanceret", under "Indstillinger": the copy of the site's files in R2 (Advanced.tsx)
35
35
  // morpheme "Morpheme", under "Indstillinger": the CMS's version, a newer one and its notes, an update's progress (CmsPage.tsx)
36
- // licenser "Licenser", under "Indstillinger": the licences of what the site and the editor are made with, and
37
- // licenser/<id> one with its text (Licenses.tsx)
36
+ // licenses "Licenser", under "Indstillinger": the licences of what the site and the editor are made with, and
37
+ // licenses/<id> one with its text (Licenses.tsx)
38
38
  // apps the apps that come with the site, apps/<app> one with its button to install it and, while it
39
39
  // is installed, the tabs of its page (AppList.tsx AppPage): Settings at apps/<app>, with its settings
40
- // form (also on the screen with its category, maaling/<app>, AppList.tsx settingsRoute), and
41
- // Information at apps/<app>/om (before an app's own screens, AppDefinition.screens)
42
- // apps/<app>/indstillinger/<field> the page of a field of its settings with its choices on a page of their own
43
- // (Field.page), over its Settings tab, which keeps what is in it: apps/content-colors/indstillinger/variant
40
+ // form (also on the screen with its category, analytics/<app>, AppList.tsx settingsRoute), and
41
+ // Information at apps/<app>/about (before an app's own screens, AppDefinition.screens)
42
+ // apps/<app>/settings/<field> the page of a field of its settings with its choices on a page of their own
43
+ // (Field.page), over its Settings tab, which keeps what is in it: apps/content-colors/settings/variant
44
44
  // <form>/<field> the page of a field of a part's form with its choices on a page of their own (Field.page), over the
45
45
  // form, which keeps what is in it (formAt)
46
- // blokke the blocks, and blokke/<block> the texts all its copies have (blockTexts.ts)
47
- // slettet/<id> what was deleted from a list, or slettet/filer the files (Deleted.tsx)
46
+ // blocks the blocks, and blocks/<block> the texts all its copies have (blockTexts.ts)
47
+ // deleted/<id> what was deleted from a list, or deleted/files the files (Deleted.tsx)
48
48
  // <file path> a file being edited, e.g. src/content/blog/mit-foerste-indlaeg.md
49
49
  import { useEffect, useMemo, useRef, useState } from 'react';
50
50
  import {
@@ -276,9 +276,9 @@ function routeOf(hash: string) {
276
276
  const route = movedRoute(screenOf(hash), STORE_MOVED);
277
277
  // An app's settings: on the screen with its category, when it has one ("Analytics", AppList.tsx settingsRoute), and so
278
278
  // the pages of their fields (Field.page) after them. Otherwise they are on the Settings tab of its page: apps/<id>
279
- // (a field's page, apps/<id>/indstillinger/<field>, stays, drawn by the same page).
280
- const app = apps.find((a) => route === `apps/${a.id}/indstillinger` || route.startsWith(`apps/${a.id}/indstillinger/`));
281
- const rest = app ? route.slice(`apps/${app.id}/indstillinger`.length) : '';
279
+ // (a field's page, apps/<id>/settings/<field>, stays, drawn by the same page).
280
+ const app = apps.find((a) => route === `apps/${a.id}/settings` || route.startsWith(`apps/${a.id}/settings/`));
281
+ const rest = app ? route.slice(`apps/${app.id}/settings`.length) : '';
282
282
  const kindScreen = app && settingsHome(app) && settingsRoute(app);
283
283
  const moved = !app ? route : kindScreen ? `${kindScreen}${rest}` : rest ? route : `apps/${app.id}`;
284
284
  if (moved !== screenOf(hash)) history.replaceState(history.state, '', `#${moved}`);
@@ -557,7 +557,7 @@ function Editor() {
557
557
  replace([file, ...(more?.files ?? [])], more?.removed);
558
558
  // A new entry, or a post that got a new address: from now on it is edited as the file it
559
559
  // became. A form that saves another file than the one at its address (Virksomheden) stays where it is.
560
- if (route.startsWith('ny/') || more?.removed.includes(route)) {
560
+ if (route.startsWith('new/') || more?.removed.includes(route)) {
561
561
  history.replaceState(history.state, '', `#${file.path}`);
562
562
  setRoute(file.path);
563
563
  }
@@ -578,7 +578,7 @@ function Editor() {
578
578
  replace([], [file.path]);
579
579
  // Back to the list, as with the back arrow: the deleted entry is not left behind it in the history.
580
580
  setUnsaved(false);
581
- up(`#liste/${collection.id}`);
581
+ up(`#list/${collection.id}`);
582
582
  notify({ text: savedText(change), kind: 'saved' });
583
583
  } catch (e) {
584
584
  notify({ text: t.app.notDeleted((e as Error).message), error: true, kind: 'failed' });
@@ -668,7 +668,7 @@ function Editor() {
668
668
  }
669
669
  >
670
670
  <>
671
- <div className="editor-content min-w-0 flex-1">
671
+ <div className="editor-content relative min-w-0 flex-1">
672
672
  {files ? (
673
673
  <Page key={visit} route={route} files={files} env={env} user={user} builtAt={builtAt} onLogout={signOut} />
674
674
  ) : (
@@ -689,15 +689,15 @@ function Editor() {
689
689
  /** The part whose form is at an address (CompanyForm.tsx): one of "Virksomheden"'s (or of its parts', with the settings of the apps of their kinds), "Søgemaskineoptimering"'s, "Udseende"'s, or one on a screen of parts with
690
690
  * the settings of the apps of its kinds ("Analytics"). */
691
691
  function partAt(route: string, files: RepoFile[]): Part | null {
692
- // virksomheden/<part>, or virksomheden/<part>/<its part> (Part.parts).
692
+ // company/<part>, or company/<part>/<its part> (Part.parts).
693
693
  const [, companyId, ownId, ...more] = route.split('/');
694
- const company = route.startsWith('virksomheden/') && !more.length && companyParts.find((p) => p.id === companyId);
694
+ const company = route.startsWith('company/') && !more.length && companyParts.find((p) => p.id === companyId);
695
695
  return (
696
696
  (company &&
697
697
  (ownId ? [...(company.parts ?? []), ...appPartsIn(company, files)].find((p) => p.id === ownId) : company.file && company)) ||
698
- (route.startsWith('blokke/') && blockParts.find((p) => p.id === route.slice(7))) ||
698
+ (route.startsWith('blocks/') && blockParts.find((p) => p.id === route.slice(7))) ||
699
699
  (route.startsWith('seo/') && seoParts.find((p) => p.id === route.slice(4))) ||
700
- (route.startsWith('udseende/') && appearance.find((p) => p.id === route.slice(9))) ||
700
+ (route.startsWith('appearance/') && appearance.find((p) => p.id === route.slice('appearance/'.length))) ||
701
701
  partScreens.flatMap((s) => [...s.parts, ...appPartsIn(s, files)].filter((p) => route === `${s.id}/${p.id}`))[0] ||
702
702
  null
703
703
  );
@@ -718,13 +718,13 @@ type PageProps = { route: string; files: RepoFile[]; env: Env; user: Viewer | nu
718
718
 
719
719
  function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
720
720
  if (!route) return <Start builtAt={builtAt} />;
721
- if (route === 'konto') return <Account user={user} files={files} onLogout={onLogout} />;
722
- if (route === 'notifikationer') return <Notifications />;
723
- if (route === 'lister') return <Lists files={files} />;
724
- if (route === 'virksomheden') return <Company files={files} />;
725
- if (route === 'blokke') return <Blocks files={files} />;
726
- if (route === 'filer' || route.startsWith('filer/'))
727
- return <Media key={route} files={files} env={env} open={route === 'filer' ? undefined : '/' + route.slice('filer/'.length)} />;
721
+ if (route === 'account') return <Account user={user} files={files} onLogout={onLogout} />;
722
+ if (route === 'notifications') return <Notifications />;
723
+ if (route === 'lists') return <Lists files={files} />;
724
+ if (route === 'company') return <Company files={files} />;
725
+ if (route === 'blocks') return <Blocks files={files} />;
726
+ if (route === 'files' || route.startsWith('files/'))
727
+ return <Media key={route} files={files} env={env} open={route === 'files' ? undefined : '/' + route.slice('files/'.length)} />;
728
728
  if (route === 'seo') return <Seo />;
729
729
  if (route === REDIRECTS_ROUTE) return <Redirects env={env} />;
730
730
  if (route === SEO_SCORES_ROUTE) return <SeoScores files={files} env={env} />;
@@ -735,22 +735,23 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
735
735
  // A part of a screen of parts that is one of the editor's own screens (Part.opens), going back to that screen.
736
736
  const opening = partScreens.flatMap((s) => s.parts.filter((p) => p.opens && route === `${s.id}/${p.id}`).map((p) => ({ s, p })))[0];
737
737
  if (opening?.p.opens === 'consents') return <Consents env={env} back={{ href: `#${opening.s.id}`, label: opening.s.label }} />;
738
- if (route === 'udseende') return <Theme files={files} />;
739
- if (route === 'udseende/temaer') return <Themes env={env} files={files} />;
740
- if (route === 'udseende/ikonpakker') return <IconPacks env={env} files={files} />;
741
- if (route === 'udseende/bevaegelse') return <MotionSchemes env={env} files={files} />;
742
- if (route === 'udseende/sidekolonne') return <SideColumn env={env} files={files} />;
743
- if (route === 'udseende/header') return <HeaderSettings env={env} />;
744
- if (route === 'udseende/footer') return <FooterSettings env={env} />;
738
+ if (route === 'appearance') return <Theme files={files} />;
739
+ if (route === 'appearance/themes') return <Themes env={env} files={files} />;
740
+ if (route === 'appearance/icon-packs') return <IconPacks env={env} files={files} />;
741
+ if (route === 'appearance/motion') return <MotionSchemes env={env} files={files} />;
742
+ if (route === 'appearance/side-column') return <SideColumn env={env} files={files} />;
743
+ if (route === 'appearance/header') return <HeaderSettings env={env} />;
744
+ if (route === 'appearance/footer') return <FooterSettings env={env} />;
745
745
  // A theme installed has its page; one from the store that is not (or not yet built), its page in the store.
746
- const themeShown = route.startsWith('udseende/temaer/') && themes.find((x) => x.id === route.slice(16));
746
+ const themeShown = route.startsWith('appearance/themes/') && themes.find((x) => x.id === route.slice('appearance/themes/'.length));
747
747
  if (themeShown) return <ThemePage key={themeShown.id} of={themeShown} env={env} files={files} />;
748
- if (/^udseende\/temaer\/[^/]+$/.test(route)) return <StoreThemePage key={route} id={route.slice(16)} env={env} />;
748
+ if (/^appearance\/themes\/[^/]+$/.test(route))
749
+ return <StoreThemePage key={route} id={route.slice('appearance/themes/'.length)} env={env} />;
749
750
  // The colours, or the site's own on a page of the same form while an app changes the pages' (FALLBACK_ROUTE).
750
- if (route === 'udseende/farver' || route === FALLBACK_ROUTE)
751
+ if (route === 'appearance/colors' || route === FALLBACK_ROUTE)
751
752
  return <Colors env={env} files={files} fallback={route === FALLBACK_ROUTE} />;
752
- if (route === 'udseende/logo') return <BrandForm env={env} />;
753
- if (route === 'blokvalg') return <BlockSettings env={env} files={files} />;
753
+ if (route === 'appearance/logo') return <BrandForm env={env} />;
754
+ if (route === 'block-settings') return <BlockSettings env={env} files={files} />;
754
755
  if (route === ADVANCED_ROUTE) return <Advanced env={env} />;
755
756
  if (route === CMS_ROUTE) return <CmsPage />;
756
757
  if (route === LICENSES_ROUTE) return <Licenses />;
@@ -761,7 +762,7 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
761
762
  if (route.startsWith(`${DEVELOPER_ROUTE}/`))
762
763
  return <DeveloperPage key={route} slug={route.slice(DEVELOPER_ROUTE.length + 1)} env={env} files={files} />;
763
764
  // An app in the site (its own, or one from the store while it is installed) has its page, with the tabs Settings
764
- // (apps/<id>, and a settings field's page, apps/<id>/indstillinger/<field>) and Information (apps/<id>/om, matched
765
+ // (apps/<id>, and a settings field's page, apps/<id>/settings/<field>) and Information (apps/<id>/about, matched
765
766
  // before an app's own screens); one from the store that is not (or no longer) installed, its page in the store.
766
767
  const shownApp = route.startsWith('apps/') ? apps.find((a) => appRouteOf(route, a.id)) : undefined;
767
768
  const shown = shownApp && appRouteOf(route, shownApp.id);
@@ -776,29 +777,29 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
776
777
  route.startsWith('apps/') && !deeper.length && apps.find((a) => a.id === screenApp && screenId && a.screens?.[screenId]);
777
778
  if (ownScreen && installedApps(files).includes(ownScreen.id))
778
779
  return <AppScreenPage key={route} app={ownScreen} id={screenId} env={env} />;
779
- if (route === 'udseende/dele') return <PartsHome />;
780
- if (route === 'udseende/dele/editor') return <Parts env={env} />;
781
- if (route === 'udseende/dele/ui') return <ComponentParts />;
782
- if (route === 'udseende/dele/blokke') return <BlocksParts />;
783
- if (route.startsWith('udseende/dele/liste/')) return <KindParts key={route} id={route.slice(20)} />;
784
- if (route === 'udseende/dele/vaerdier') return <Tokens />;
785
- if (route.startsWith('slettet/')) return <Deleted id={route.slice(8)} files={files} env={env} />;
786
- // virksomheden/<part>, or virksomheden/<part>/<its part> (Part.parts).
780
+ if (route === 'appearance/parts') return <PartsHome />;
781
+ if (route === 'appearance/parts/editor') return <Parts env={env} />;
782
+ if (route === 'appearance/parts/ui') return <ComponentParts />;
783
+ if (route === 'appearance/parts/blocks') return <BlocksParts />;
784
+ if (route.startsWith('appearance/parts/list/')) return <KindParts key={route} id={route.slice('appearance/parts/list/'.length)} />;
785
+ if (route === 'appearance/parts/values') return <Tokens />;
786
+ if (route.startsWith('deleted/')) return <Deleted id={route.slice('deleted/'.length)} files={files} env={env} />;
787
+ // company/<part>, or company/<part>/<its part> (Part.parts).
787
788
  const [, companyId, ownId, ...more] = route.split('/');
788
- const company = route.startsWith('virksomheden/') && !more.length && companyParts.find((p) => p.id === companyId);
789
+ const company = route.startsWith('company/') && !more.length && companyParts.find((p) => p.id === companyId);
789
790
  if (company && company.parts && !ownId) return <Company part={company} files={files} />;
790
791
  // A part's form, or the page of one of its fields (Field.page) with the form under it: the same form, so it keeps what
791
792
  // is in it while the editor goes between them (useFormPages, inForm).
792
793
  const form = formAt(route, files);
793
794
  if (form) return <CompanyForm part={form.part} at={form.at} page={form.page} env={env} files={files} />;
794
- // A list, or a tab an installed app adds to it (liste/<id>/<app>).
795
+ // A list, or a tab an installed app adds to it (list/<id>/<app>).
795
796
  const [, listId, listTab, ...below] = route.split('/');
796
- const listed = route.startsWith('liste/') && !below.length && collections.find((c) => c.id === listId);
797
+ const listed = route.startsWith('list/') && !below.length && collections.find((c) => c.id === listId);
797
798
  if (listed) return <List key={listed.id} collection={listed} tab={listTab} files={files} env={env} />;
798
799
 
799
- // ny/<list>, or ny/<list>/<kind> for one of its kinds (a pop-up: standard, mailchimp).
800
+ // new/<list>, or new/<list>/<kind> for one of its kinds (a pop-up: standard, mailchimp).
800
801
  const [, newIn, newKind] = route.split('/');
801
- const creating = route.startsWith('ny/') && collections.find((c) => c.id === newIn && c.create);
802
+ const creating = route.startsWith('new/') && collections.find((c) => c.id === newIn && c.create);
802
803
  if (creating) {
803
804
  if (creating.kind === 'markdown') return <EntryForm collection={creating} file={newMarkdown(creating, notOffered(files))} env={env} />;
804
805
  if (creating.kind === 'folder')
package/src/AppAdds.tsx CHANGED
@@ -29,7 +29,7 @@ function addsOf(app: SiteApp, files: RepoFile[]) {
29
29
  ...kindsOf(app).map((k) => ({
30
30
  ...k,
31
31
  block: undefined,
32
- create: k.create ? { href: `#ny/${k.key}/${app.id}`, label: k.create } : undefined,
32
+ create: k.create ? { href: `#new/${k.key}/${app.id}`, label: k.create } : undefined,
33
33
  uses: entriesOf(app, k.key, files),
34
34
  })),
35
35
  ];
package/src/AppList.tsx CHANGED
@@ -2,7 +2,7 @@
2
2
  // with one from the store not yet built as "Installeres…"), each with who made it under its name and, from the store,
3
3
  // that it has a newer version, with search (by name, who made it and what it does), the order (A–Å, Å–A, kept in this
4
4
  // browser) and headings by the first letter, as the lists under "Indhold" have. "Indstillinger" › "Appstore"
5
- // (#indstillinger/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
5
+ // (#settings/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
6
6
  // do, who made them and the words they are found by), the same order and a funnel by category. An app's page
7
7
  // (#apps/<app>) is the same for every app and without examples: its name, who made it, whether it is installed, its
8
8
  // version (from the store) and what it does, "Installer" (or "Opdater til …"); installed, it has two tabs: "Indstillinger"
@@ -11,10 +11,10 @@
11
11
  // adds (AppAdds.tsx: its blocks, the lists it brings: "Priser", each opening its list, and its kinds of the site's lists:
12
12
  // a Mailchimp popup; each with its switch at the end of its row and opening a sheet with where it is used and the same
13
13
  // switch, as under "Blokindstillinger", and a kind's button to a new one, while it is on), its notifications with their
14
- // switches, and "Afinstaller", AppSettingsTab.tsx) and "Information" (#apps/<app>/om, AppInfoTab.tsx: its pictures, what
14
+ // switches, and "Afinstaller", AppSettingsTab.tsx) and "Information" (#apps/<app>/about, AppInfoTab.tsx: its pictures, what
15
15
  // it does more fully ("Om appen"), what it needs of the site ("Rettigheder", needsOf in store.ts: its code on every page,
16
16
  // the addresses it loads from and sends forms to, its part of the Worker, a step in the build), the cookies it sets (and
17
- // whether the cookie banner asks first), who made it (a row to their page, #apps/udvikler/<name>, with all their apps),
17
+ // whether the cookie banner asks first), who made it (a row to their page, #apps/developer/<name>, with all their apps),
18
18
  // and where it is used); one not installed has no tabs; installing, updating and uninstalling are each saved at once in its own commit,
19
19
  // as a block turned on or off under "Blokindstillinger". The button pressed shows the loading indicator while it runs (busyAction.mjs). One of the site's own is installed or uninstalled in the site's
20
20
  // file of apps; one from the store is copied into the site, or deleted from it, with it (appChanges in store.ts), and
@@ -49,6 +49,11 @@ import { AnyIcon } from './icon';
49
49
  import {
50
50
  appCategories,
51
51
  appChanges,
52
+ appliedTo,
53
+ installedHere,
54
+ legacyOf,
55
+ mergeChanges,
56
+ moveChanges,
52
57
  appFolder,
53
58
  breaking,
54
59
  changedFiles,
@@ -64,7 +69,7 @@ import {
64
69
  type StoreAdd,
65
70
  type StoreRecord,
66
71
  } from './store';
67
- import { apps, siteBlocks } from './registry';
72
+ import { apps, apps as siteApps, siteBlocks } from './registry';
68
73
  import { asWritten, backFrom, collections, companyParts, partScreens } from './site';
69
74
  import { byText, slugify, type Part } from './schema';
70
75
  import { headed, letterOf } from './Sheet';
@@ -94,10 +99,10 @@ export function settingsHome(app: SiteApp): { route: string; label: string } | u
94
99
  const screen = partScreens.find((s) => s.apps?.includes(kind));
95
100
  if (screen) return { route: screen.id, label: screen.label };
96
101
  const company = companyParts.find((p) => p.parts && p.apps?.includes(kind));
97
- return company && { route: `virksomheden/${company.id}`, label: company.label };
102
+ return company && { route: `company/${company.id}`, label: company.label };
98
103
  }
99
104
 
100
- /** Where an app's settings are: on the screen with its kind (#maaling/google-analytics, #virksomheden/betaling/mobilepay),
105
+ /** Where an app's settings are: on the screen with its kind (#analytics/google-analytics, #company/payment/mobilepay),
101
106
  * else on the Settings tab of its page (#apps/<id>); null without settings. */
102
107
  export function settingsRoute(app: SiteApp): string | null {
103
108
  if (!app.settings) return null;
@@ -156,7 +161,7 @@ export const listsOf = (app: SiteApp, files: RepoFile[]) =>
156
161
  key: l.id,
157
162
  icon: l.icon,
158
163
  label: l.label,
159
- href: `#liste/${l.id}`,
164
+ href: `#list/${l.id}`,
160
165
  count: files.filter((f) => f.path.startsWith(`${l.folder}/`) && f.path.endsWith('.json')).length,
161
166
  }));
162
167
 
@@ -186,7 +191,7 @@ export function kindsOf(app: SiteApp) {
186
191
  icon: kind.icon,
187
192
  label: kind.label,
188
193
  hint: kind.hint,
189
- href: `#liste/${c.id}`,
194
+ href: `#list/${c.id}`,
190
195
  one: c.singular,
191
196
  more: c.label,
192
197
  create: c.create,
@@ -273,7 +278,7 @@ const versionOf = (app: SiteApp) => updated.get(app.id) ?? app.store?.version ??
273
278
 
274
279
  /** The newer version of an app from the store, when the store has one. */
275
280
  const updateOf = (app: SiteApp, catalog: ShownApp[] | null | 'failed') => {
276
- const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
281
+ const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id || a.legacy === app.id) : undefined;
277
282
  return found && newer(found.version, versionOf(app)) ? found : undefined;
278
283
  };
279
284
 
@@ -284,7 +289,7 @@ export function Apps({ env, files }: { env: Env; files: RepoFile[] }) {
284
289
  const installed = installedApps(files);
285
290
  const catalog = useCatalog(env);
286
291
  const store = Array.isArray(catalog) ? catalog : [];
287
- const [query, setQuery] = useKept('apps-soeg', '');
292
+ const [query, setQuery] = useKept('apps-search', '');
288
293
  const [sort, pickSort] = useSort();
289
294
 
290
295
  // The apps installed: the site's own and those from the store; and one from the store in the file of apps that the
@@ -396,7 +401,7 @@ function AppRow({ app, installed }: { app: ReturnType<typeof allApps>[number]; i
396
401
  );
397
402
  }
398
403
 
399
- /** A developer's page (#apps/udvikler/<name>): their name, and all their apps and themes (in the store and the site's
404
+ /** A developer's page (#apps/developer/<name>): their name, and all their apps and themes (in the store and the site's
400
405
  * own), each opening its page. */
401
406
  export function DeveloperPage({ slug, env, files }: { slug: string; env: Env; files: RepoFile[] }) {
402
407
  const installed = installedApps(files);
@@ -457,9 +462,9 @@ export function DeveloperRow({ name }: { name: string }) {
457
462
  );
458
463
  }
459
464
 
460
- /** "Appstore" (#indstillinger/appstore, under "Indstillinger"): in a tab of its own each in the top app bar, all apps,
465
+ /** "Appstore" (#settings/appstore, under "Indstillinger"): in a tab of its own each in the top app bar, all apps,
461
466
  * those in the app store and the site's own, each with what it does and whether it is installed, and all themes
462
- * (#indstillinger/appstore/temaer, ThemeStore.tsx), those in the store
467
+ * (#settings/appstore/themes, ThemeStore.tsx), those in the store
463
468
  * and those installed, each with its version and who made it; each opening its page. With search (by name, what it
464
469
  * does, who made it and the words it is found by, in both languages), the order and the filters by category (apps)
465
470
  * and by whether it is installed, each picked one as a chip. */
@@ -478,10 +483,10 @@ function StoreScreen({ env, files, tab, again }: { env: Env; files: RepoFile[];
478
483
  const catalog = useCatalog(env);
479
484
  const themeCatalog = useThemeCatalog(env);
480
485
  const connected = useStoreConnected(env);
481
- const [query, setQuery] = useKept('butik-soeg', '');
486
+ const [query, setQuery] = useKept('store-search', '');
482
487
  const [sort, pickSort] = useSort();
483
- const [category, setCategory] = useKept<AppCategory | ''>('butik-kategori', '');
484
- const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('butik-status', '');
488
+ const [category, setCategory] = useKept<AppCategory | ''>('store-category', '');
489
+ const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('store-status', '');
485
490
  const apps = tab === 'apps';
486
491
  const read = apps ? catalog : themeCatalog;
487
492
 
@@ -491,7 +496,7 @@ function StoreScreen({ env, files, tab, again }: { env: Env; files: RepoFile[];
491
496
  allApps(catalog)
492
497
  .filter((a) => words.every((w) => a.words.includes(w)))
493
498
  .filter((a) => !category || a.category === category)
494
- .filter((a) => !state || installed.includes(a.id) === (state === 'installed'))
499
+ .filter((a) => !state || installedHere(a, installed, siteApps) === (state === 'installed'))
495
500
  .sort((a, b) => order(a.label, b.label)),
496
501
  (a) => letterOf(a.label),
497
502
  );
@@ -692,11 +697,13 @@ function useStoreActions(env: Env, files: RepoFile[]) {
692
697
  next: () => Promise<Parameters<typeof appChanges>[2]>,
693
698
  // Whether to go on, with the app's files in the site now (an update asks, when some were changed).
694
699
  go: (current: RepoFile[]) => Promise<boolean> = async () => true,
700
+ // The id the app is installed under, when it is a legacy copy that moves to `id` (moveChanges).
701
+ legacy: string | null = null,
695
702
  ): Promise<boolean> {
696
703
  setRunning({ id, action, asking: false });
697
704
  env.notify({ text: text.busy });
698
705
  try {
699
- const [current, wanted] = await Promise.all([env.folder(appFolder(id)), next()]);
706
+ const [current, wanted] = await Promise.all([env.folder(appFolder(legacy ?? id)), next()]);
700
707
  if (!(await go(current))) {
701
708
  env.notify(null);
702
709
  return false;
@@ -704,8 +711,11 @@ function useStoreActions(env: Env, files: RepoFile[]) {
704
711
  // `go` cleared the message (and its dialog stopped the button being busy): saving goes on.
705
712
  setRunning({ id, action, asking: false });
706
713
  env.notify({ text: text.busy });
707
- const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, files);
708
- await saveApp(env, text.commit, text.change, [...appChanges(id, current, wanted, installed, file), ...cookies], file);
714
+ const moved = legacy && wanted ? moveChanges({ id }, legacy, wanted, current, files) : null;
715
+ // The cookie list as the move leaves it (the legacy id rewritten), then the new release's cookies on it: one set of changes.
716
+ const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, moved ? appliedTo(files, moved) : files);
717
+ const changes = moved ? mergeChanges(moved, cookies) : [...appChanges(id, current, wanted, installed, file), ...cookies];
718
+ await saveApp(env, text.commit, text.change, changes, file);
709
719
  env.notify({ text: text.done, kind: text.kind ?? 'saved' });
710
720
  return true;
711
721
  } catch (e) {
@@ -731,7 +741,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
731
741
  },
732
742
  async () => ({ files: await env.store.files(app), record: recordOf(app) }),
733
743
  ),
734
- async update(app: ShownApp, record: StoreRecord) {
744
+ async update(app: ShownApp, record: StoreRecord, legacy: string | null = null) {
735
745
  const done = await change(
736
746
  app.id,
737
747
  'update',
@@ -751,9 +761,10 @@ function useStoreActions(env: Env, files: RepoFile[]) {
751
761
  record.version,
752
762
  app.version,
753
763
  notesSince(app.releaseNotes, record.version, app.version),
754
- changedFiles(app.id, record, current),
764
+ changedFiles(app.id, record, current, appFolder(legacy ?? app.id)),
755
765
  );
756
766
  },
767
+ legacy,
757
768
  );
758
769
  if (done) updated.set(app.id, app.version);
759
770
  return done;
@@ -937,7 +948,7 @@ type AppProps = { app: SiteApp; env: Env; files: RepoFile[] };
937
948
 
938
949
  type AppTab = 'settings' | 'information';
939
950
 
940
- /** The tabs of an installed app's page, in the top bar: Settings (#apps/<id>) and Information (#apps/<id>/om). A form
951
+ /** The tabs of an installed app's page, in the top bar: Settings (#apps/<id>) and Information (#apps/<id>/about). A form
941
952
  * with unsaved changes asks first, as for every address (App.tsx). */
942
953
  const tabsOf = (app: SiteApp, tab: AppTab): TabChoice<AppTab> => ({
943
954
  label: t.apps.tabs,
@@ -1047,7 +1058,11 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
1047
1058
  {(!on || (update && app.store)) && (
1048
1059
  <div className="mb-8">
1049
1060
  {on ? (
1050
- <Button disabled={saving} busy={store.busy(app.id, 'update')} onClick={() => store.update(update!, app.store!)}>
1061
+ <Button
1062
+ disabled={saving}
1063
+ busy={store.busy(update!.id, 'update')}
1064
+ onClick={() => store.update(update!, app.store!, legacyOf(update!, apps))}
1065
+ >
1051
1066
  {t.apps.update(update!.version)}
1052
1067
  </Button>
1053
1068
  ) : (