@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
@@ -4,21 +4,138 @@ import assert from 'node:assert/strict';
4
4
  import { movedRoute } from './movedRoutes.mjs';
5
5
 
6
6
  test('the old header screen opens Appearance › Header', () => {
7
- assert.equal(movedRoute('header'), 'udseende/header');
7
+ assert.equal(movedRoute('header'), 'appearance/header');
8
8
  });
9
9
 
10
10
  test('the old footer form opens Appearance › Footer', () => {
11
- assert.equal(movedRoute('src/data/footer.json'), 'udseende/footer');
11
+ assert.equal(movedRoute('src/data/footer.json'), 'appearance/footer');
12
12
  });
13
13
 
14
14
  test('an address that has not moved stays', () => {
15
- assert.equal(movedRoute('udseende/header'), 'udseende/header');
16
- assert.equal(movedRoute('lister'), 'lister');
15
+ assert.equal(movedRoute('appearance/header'), 'appearance/header');
16
+ assert.equal(movedRoute('lists'), 'lists');
17
17
  });
18
18
 
19
19
  test('the extra moves come first', () => {
20
- assert.equal(movedRoute('butik', { butik: 'indstillinger/appstore' }), 'indstillinger/appstore');
21
- assert.equal(movedRoute('tema', { butik: 'x' }), 'udseende');
20
+ assert.equal(movedRoute('butik', { butik: 'settings/appstore' }), 'settings/appstore');
21
+ assert.equal(movedRoute('tema', { butik: 'x' }), 'appearance');
22
+ });
23
+
24
+ test('only the old prefix of an address is renamed, the rest stays', () => {
25
+ const renamed = { indstillinger: 'settings', 'udseende/dele': 'appearance/parts', liste: 'list' };
26
+ assert.equal(movedRoute('indstillinger/appstore', {}, renamed), 'settings/appstore');
27
+ assert.equal(movedRoute('udseende/dele/liste/kontakt', {}, renamed), 'appearance/parts/liste/kontakt');
28
+ });
29
+
30
+ test('a key segment * matches any one segment and keeps it', () => {
31
+ const renamed = { 'apps/*/indstillinger': 'apps/*/settings' };
32
+ assert.equal(movedRoute('apps/x/indstillinger/variant', {}, renamed), 'apps/x/settings/variant');
33
+ assert.equal(movedRoute('apps/x/om', {}, renamed), 'apps/x/om');
34
+ });
35
+
36
+ test('data inside an address is not renamed', () => {
37
+ assert.equal(movedRoute('files/filer/doc.pdf'), 'files/filer/doc.pdf');
38
+ assert.equal(movedRoute('blog/farver'), 'blog/farver');
39
+ assert.equal(movedRoute('list/temaer'), 'list/temaer');
40
+ });
41
+
42
+ test('old prefixes are renamed, the data after them stays', () => {
43
+ assert.equal(movedRoute('filer/filer/doc.pdf'), 'files/filer/doc.pdf');
44
+ assert.equal(movedRoute('liste/temaer'), 'list/temaer');
45
+ assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
46
+ assert.equal(movedRoute('indstillinger/appstore'), 'settings/appstore');
47
+ assert.equal(movedRoute('udseende/farver'), 'appearance/colors');
48
+ });
49
+
50
+ test('every old route prefix opens the screen where it is now', () => {
51
+ const cases = {
52
+ indstillinger: 'settings',
53
+ 'indstillinger/appstore/temaer': 'settings/appstore/themes',
54
+ 'apps/google-analytics/indstillinger': 'apps/google-analytics/settings',
55
+ 'apps/udvikler/acme': 'apps/developer/acme',
56
+ 'apps/cms-version/om': 'apps/cms-version/about',
57
+ 'apps/content-colors/indstillinger/variant': 'apps/content-colors/settings/variant',
58
+ udseende: 'appearance',
59
+ 'udseende/temaer': 'appearance/themes',
60
+ 'udseende/temaer/material': 'appearance/themes/material',
61
+ 'udseende/ikonpakker': 'appearance/icon-packs',
62
+ 'udseende/bevaegelse': 'appearance/motion',
63
+ 'udseende/sidekolonne': 'appearance/side-column',
64
+ 'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
65
+ 'udseende/dele': 'appearance/parts',
66
+ 'udseende/dele/editor': 'appearance/parts/editor',
67
+ 'udseende/dele/blokke': 'appearance/parts/blocks',
68
+ 'udseende/dele/vaerdier': 'appearance/parts/values',
69
+ 'udseende/standardsider': 'appearance/default-pages',
70
+ 'udseende/logo': 'appearance/logo',
71
+ maaling: 'analytics',
72
+ 'maaling/google-analytics': 'analytics/google-analytics',
73
+ privatliv: 'privacy',
74
+ 'privatliv/cookiebanner': 'privacy/cookie-banner',
75
+ 'privatliv/samtykker': 'privacy/consents',
76
+ 'privatliv/privatlivssider': 'privacy/privacy-pages',
77
+ lister: 'lists',
78
+ liste: 'list',
79
+ 'liste/priser/cookiescanner': 'list/priser/cookiescanner',
80
+ 'liste/kategorier': 'list/tags',
81
+ ny: 'new',
82
+ 'ny/popups/mailchimp': 'new/popups/mailchimp',
83
+ 'ny/kategorier': 'new/tags',
84
+ 'ny/udtalelser': 'new/testimonials',
85
+ slettet: 'deleted',
86
+ 'slettet/priser': 'deleted/priser',
87
+ 'slettet/kategorier': 'deleted/tags',
88
+ 'slettet/udtalelser': 'deleted/testimonials',
89
+ 'liste/udtalelser': 'list/testimonials',
90
+ 'liste/mobilmenu': 'list/mobile-menu',
91
+ 'slettet/mobilmenu': 'deleted/mobile-menu',
92
+ 'slettet/filer': 'deleted/files',
93
+ virksomheden: 'company',
94
+ 'virksomheden/kontakt': 'company/contact',
95
+ 'virksomheden/sociale-medier': 'company/social-media',
96
+ 'virksomheden/betaling/mobilepay': 'company/payment/mobilepay',
97
+ blokke: 'blocks',
98
+ 'blokke/accordion': 'blocks/accordion',
99
+ filer: 'files',
100
+ 'seo/videresendelser': 'seo/redirects',
101
+ 'seo/videresendelser/alle': 'seo/redirects/all',
102
+ 'seo/videresendelser/ikke-fundet': 'seo/redirects/not-found',
103
+ 'seo/interne-links': 'seo/internal-links',
104
+ 'seo/vurdering': 'seo/rating',
105
+ blokvalg: 'block-settings',
106
+ avanceret: 'advanced',
107
+ licenser: 'licenses',
108
+ konto: 'account',
109
+ notifikationer: 'notifications',
110
+ 'licenser/mit': 'licenses/mit',
111
+ };
112
+ for (const [old, now] of Object.entries(cases)) assert.equal(movedRoute(old), now, old);
113
+ });
114
+
115
+ test('a new address stays', () => {
116
+ assert.equal(movedRoute('settings/appstore'), 'settings/appstore');
117
+ assert.equal(movedRoute('blog/mit-indlaeg'), 'blog/mit-indlaeg');
118
+ });
119
+
120
+ test('a moved address wins over a renamed segment', () => {
121
+ assert.equal(movedRoute('tema', {}, { tema: 'x' }), 'appearance');
122
+ });
123
+
124
+ test('an old route to the app store opens it at its English route', () => {
125
+ assert.equal(movedRoute('indstillinger/appstore'), 'settings/appstore');
126
+ });
127
+
128
+ test('the old colours route opens the colours screen under Appearance', () => {
129
+ assert.equal(movedRoute('farver'), 'appearance/colors');
130
+ });
131
+
132
+ test('an old parts route keeps a list id and opens at its English route', () => {
133
+ assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
134
+ });
135
+
136
+ test('the old theme routes open the screens under Appearance', () => {
137
+ assert.equal(movedRoute('tema'), 'appearance');
138
+ assert.equal(movedRoute('tema/dele/editor'), 'appearance/parts/editor');
22
139
  });
23
140
 
24
141
  test('the old booking and newsletter settings open the apps by their permanent ids', () => {
package/src/nav.ts CHANGED
@@ -9,7 +9,7 @@ type Step = { at: number; prev: string | null };
9
9
 
10
10
  const step = () => history.state as Step | null;
11
11
 
12
- /** The screen of an address ("#liste/sider" → "liste/sider", "#" → ""); one with a broken %-sign is
12
+ /** The screen of an address ("#list/sider" → "list/sider", "#" → ""); one with a broken %-sign is
13
13
  * the start screen. */
14
14
  export function screenOf(hash: string) {
15
15
  try {
@@ -1,4 +1,4 @@
1
- // The notifications (#notifikationer, Notifications.tsx): what the editor and the apps have to say that is worth
1
+ // The notifications (#notifications, Notifications.tsx): what the editor and the apps have to say that is worth
2
2
  // reading again, newest first, with when it came and the link it had. Each has a type: the editor's own (`site`: the
3
3
  // site is updated or not, `saved`: something is saved, deleted, restored or installed, `failed`: it could not be,
4
4
  // `updates`: an app from the store updated on its own, AppList.tsx autoUpdate), or
package/src/schema.ts CHANGED
@@ -77,7 +77,7 @@ export type Field = {
77
77
  * picked when it is made). */
78
78
  locked?: boolean;
79
79
  /** select: the choices on a page of their own under the form, at its address with the field's name after it
80
- * (#apps/content-colors/indstillinger/variant), in place of the sheet, drawn by this with what each gives (a picture of
80
+ * (#apps/content-colors/settings/variant), in place of the sheet, drawn by this with what each gives (a picture of
81
81
  * it); the field links there. Only a field of a part's own form (CompanyForm.tsx: an app's settings), not one in a
82
82
  * group or a list; elsewhere the choices are in the sheet. */
83
83
  page?: ComponentType<FieldPageProps>;
@@ -162,7 +162,7 @@ export type Template = {
162
162
  * bar opens the booking sheet, as "Book en tid" does), else the entry itself opens. */
163
163
  export type Opens = { label: string; as?: string };
164
164
 
165
- /** A place in the editor (a route, e.g. 'virksomheden/kontakt') and the text of the link to it. */
165
+ /** A place in the editor (a route, e.g. 'company/contact') and the text of the link to it. */
166
166
  export type Edit = { to: string; text: string };
167
167
 
168
168
  // A kind of item in a list of blocks (a fixed page's sections; in the editor cards in its text, SectionsField.tsx).
package/src/site.ts CHANGED
@@ -9,7 +9,7 @@ import { appIdOf, installedApps, withKinds, type AppDefinition, type AppList, ty
9
9
  import { appBlocks, appFiles, appFolders, appPictures, appShared, appStores } from './appFiles';
10
10
  import type { BlockDefinition, SiteBlock } from './blocks';
11
11
  import type { ListRows } from './fields';
12
- import { siteT, siteText, t, translatingSite } from './i18n';
12
+ import { inSiteLanguage, siteT, siteText, t, translatingSite } from './i18n';
13
13
  import { byText, type Block, type Collection, type DataFile, type Edit, type Field, type OwnedList, type Part } from './schema';
14
14
  import { blockTypeOf, type SharedTexts } from './shared';
15
15
  import { appFolder, needsOf, SCREENSHOTS, shotFile } from './store';
@@ -19,17 +19,40 @@ const written = new WeakMap<object, object>();
19
19
  /** A collection, a file, a part or a block as the site wrote it, in its language: for the commits. */
20
20
  export const asWritten = <T extends object>(shown: T): T => (written.get(shown) as T | undefined) ?? shown;
21
21
 
22
- // Only the keys a thing has, so it is the same as it was but for its texts.
23
- function shown<T extends object>(from: T, texts: Partial<T>): T {
22
+ // Only the keys a thing has, so it is the same as it was but for its texts. `link`: remember `from` as what it was written as.
23
+ function shownAs<T extends object>(from: T, texts: Partial<T>, link: boolean): T {
24
24
  const to = { ...from };
25
25
  for (const key of Object.keys(texts) as (keyof T)[]) if (from[key] !== undefined) to[key] = texts[key]!;
26
- written.set(to, from);
26
+ if (link) written.set(to, from);
27
27
  return to;
28
28
  }
29
29
 
30
30
  /** The site's parts with every text of theirs said by `say`: the label, the hint, the link elsewhere,
31
- * the message of a pattern, the options, a switch's two words … of each field, block, file and part. */
32
- export function translator(say: (text: string) => string) {
31
+ * the message of a pattern, the options, a switch's two words … of each field, block, file and part.
32
+ * `sayWritten`: what asWritten gives back for each thing it returns, with its texts said by it (the standard parts'
33
+ * English in the site's language, inSiteLanguage); without it, asWritten gives back the thing as it was. */
34
+ export function translator(say: (text: string) => string, sayWritten?: (text: string) => string) {
35
+ const shownTexts = build(say, !sayWritten);
36
+ if (!sayWritten) return shownTexts;
37
+ const writtenTexts = build(sayWritten, false);
38
+ const both =
39
+ <A extends object>(one: (x: A) => A, said: (x: A) => A) =>
40
+ (x: A): A => {
41
+ const to = one(x);
42
+ written.set(to, said(x));
43
+ return to;
44
+ };
45
+ return {
46
+ ...shownTexts,
47
+ block: both(shownTexts.block, writtenTexts.block),
48
+ collection: both(shownTexts.collection, writtenTexts.collection),
49
+ part: both(shownTexts.part, writtenTexts.part),
50
+ app: both(shownTexts.app, writtenTexts.app),
51
+ } as typeof shownTexts;
52
+ }
53
+
54
+ function build(say: (text: string) => string, link: boolean) {
55
+ const shown = <T extends object>(from: T, texts: Partial<T>): T => shownAs(from, texts, link);
33
56
  const maybe = (text?: string) => (text === undefined ? text : say(text));
34
57
  const edit = (e?: Edit) => e && { ...e, text: say(e.text) };
35
58
  const field = (f: Field): Field =>
@@ -94,7 +117,8 @@ export function translator(say: (text: string) => string) {
94
117
  /** A colour under "Farver" (colorFields in @site/colors): its CSS variable without "--", its name and what it is for. */
95
118
  type ColorField = (typeof site.colorFields)[number];
96
119
 
97
- const local = translator(siteText);
120
+ // A commit names a part in the site's language: the standard parts' English texts through inSiteLanguage, the site's own as written.
121
+ const local = translator(siteText, inSiteLanguage);
98
122
  const each = <T>(all: T[], one: (x: T) => T) => (translatingSite ? all.map(one) : all);
99
123
 
100
124
  // The screens under "Blokke" of the blocks with shared texts (shared.ts in the block's folder), named and
@@ -117,7 +141,7 @@ function sharedPartsOf(blocks: Folders<BlockDefinition>, shared: Folders<SharedT
117
141
  fields,
118
142
  defaults: values,
119
143
  preview: { type, ...preview },
120
- back: { href: '#blokke', label: t.common.parts.blocks },
144
+ back: { href: '#blocks', label: t.common.parts.blocks },
121
145
  },
122
146
  ];
123
147
  });
@@ -142,7 +166,7 @@ export const writtenBlockParts: Part[] = [...site.blockParts, ...sharedParts, ..
142
166
  /** The parts of "Virksomheden"; one with parts of its own (Part.parts) has them going back to it. */
143
167
  export const companyParts = each(
144
168
  site.companyParts.map((p) =>
145
- p.parts ? { ...p, parts: p.parts.map((s) => ({ ...s, back: { href: `#virksomheden/${p.id}`, label: p.label } })) } : p,
169
+ p.parts ? { ...p, parts: p.parts.map((s) => ({ ...s, back: { href: `#company/${p.id}`, label: p.label } })) } : p,
146
170
  ),
147
171
  local.part,
148
172
  );
@@ -155,9 +179,9 @@ export const partScreens = site.partScreens.map((s) => ({
155
179
  local.part,
156
180
  ),
157
181
  }));
158
- /** The site's own parts under "Udseende" (#udseende, Library.tsx Theme), after the logo and icon, each going back to it. */
182
+ /** The site's own parts under "Udseende" (#appearance, Library.tsx Theme), after the logo and icon, each going back to it. */
159
183
  export const appearance = each(
160
- site.appearance.map((p) => ({ ...p, back: { href: '#udseende', label: t.common.parts.appearance } })),
184
+ site.appearance.map((p) => ({ ...p, back: { href: '#appearance', label: t.common.parts.appearance } })),
161
185
  local.part,
162
186
  );
163
187
  /** Where a screen goes back to: the start screen. The menu's sections ("Indhold" …) are lists in the drawer, not screens,
@@ -190,7 +214,7 @@ export const placeOf = (id: string): string | undefined => groups.find((g) => g.
190
214
  * ("Udseende › Standardsider"): the part with its field in the file of the standard pages. */
191
215
  export function standardPageAt(key: string): string {
192
216
  const chosenIn = (p: Part) => p.file === site.config.standardPages && !!p.fields?.some((f) => f.name === key);
193
- const screen = [{ id: 'udseende', label: t.common.parts.appearance, parts: appearance }, ...partScreens].find((s) =>
217
+ const screen = [{ id: 'appearance', label: t.common.parts.appearance, parts: appearance }, ...partScreens].find((s) =>
194
218
  s.parts.some(chosenIn),
195
219
  );
196
220
  if (!screen) return '';
@@ -314,7 +338,7 @@ export const icons = {
314
338
  ),
315
339
  };
316
340
 
317
- /** The short lists, under "Lister" (#lister): the site's, the standard blocks' and the apps', which go back there too. */
341
+ /** The short lists, under "Lister" (#lists): the site's, the standard blocks' and the apps', which go back there too. */
318
342
  export const lists = [...site.lists, ...ownedLists.map((l) => l.id)];
319
343
 
320
344
  /** The lists under "Lister" now: the site's, the standard blocks' and those of the apps installed, without the cookie
@@ -14,7 +14,7 @@ export default defineBlock({
14
14
  label: t.blocks.accordion.label,
15
15
  icon: Rows,
16
16
  hint: t.blocks.accordion.hint,
17
- edit: { to: 'blokke/accordion', text: t.blocks.accordion.edit },
17
+ edit: { to: 'blocks/accordion', text: t.blocks.accordion.edit },
18
18
  fields: [
19
19
  ...headFields,
20
20
  {
@@ -5,7 +5,7 @@ import { siteT, t } from '../../i18n';
5
5
  import { defineShared } from '../../shared';
6
6
 
7
7
  /** The rows it is shown with where no page has it: its screen under "Blokke" and "Tilgængelige
8
- * blokke" (index.tsx). In the site's language, like the drawing. */
8
+ * blocks" (index.tsx). In the site's language, like the drawing. */
9
9
  export const example = { items: siteT.blocks.accordion.example.map((label) => ({ label, href: '' })) };
10
10
 
11
11
  export default defineShared({
@@ -11,7 +11,7 @@ export default defineBlock({
11
11
  label: t.blocks.buttons.label,
12
12
  icon: CursorClick,
13
13
  hint: t.blocks.buttons.hint,
14
- edit: { to: 'blokke/buttons', text: t.blocks.buttons.edit },
14
+ edit: { to: 'blocks/buttons', text: t.blocks.buttons.edit },
15
15
  fields: [{ ...buttonList, hint: t.blocks.buttons.itemsHint }],
16
16
  preview: true,
17
17
  detail: (p) => [
@@ -13,7 +13,7 @@ export default defineBlock({
13
13
  label: t.blocks.cards.label,
14
14
  icon: Cards,
15
15
  hint: t.blocks.cards.hint,
16
- edit: { to: 'blokke/cards', text: t.blocks.cards.edit },
16
+ edit: { to: 'blocks/cards', text: t.blocks.cards.edit },
17
17
  fields: [
18
18
  ...headFields,
19
19
  // An icon is the empty choice: so are the cards from before there was a choice.
@@ -4,7 +4,7 @@ import { siteT, t } from '../../i18n';
4
4
  import { defineShared } from '../../shared';
5
5
 
6
6
  /** The cards it is shown with where no page has it: its screen under "Blokke" and "Tilgængelige
7
- * blokke" (index.tsx). In the site's language, like the drawing. With a page, so the link's text,
7
+ * blocks" (index.tsx). In the site's language, like the drawing. With a page, so the link's text,
8
8
  * which is edited there, is on them. */
9
9
  export const example = { items: siteT.blocks.cards.example.map((c) => ({ ...c, href: '/' })) };
10
10
 
@@ -11,7 +11,7 @@ export default defineBlock({
11
11
  label: t.blocks.cookies.label,
12
12
  icon: Cookie,
13
13
  hint: t.blocks.cookies.hint,
14
- edit: { to: `liste/${config.lists.cookies}`, text: t.blocks.cookies.edit },
14
+ edit: { to: `list/${config.lists.cookies}`, text: t.blocks.cookies.edit },
15
15
  fields: [],
16
16
  example: {},
17
17
  });
@@ -49,7 +49,7 @@ export default defineBlock({
49
49
  label: t.blocks.posts.label,
50
50
  icon: Newspaper,
51
51
  hint: t.blocks.posts.hint,
52
- edit: { to: 'blokke/posts', text: t.blocks.posts.edit },
52
+ edit: { to: 'blocks/posts', text: t.blocks.posts.edit },
53
53
  fields: [
54
54
  ...headFields,
55
55
  // A list (the empty choice: so are the blocks from before there was a choice), or the newest large over the list
@@ -36,7 +36,7 @@ export default defineBlock({
36
36
  label: t.blocks.testimonials.label,
37
37
  icon: Quotes,
38
38
  hint: t.blocks.testimonials.hint,
39
- edit: { to: `liste/${config.lists.testimonials}`, text: t.blocks.testimonials.edit },
39
+ edit: { to: `list/${config.lists.testimonials}`, text: t.blocks.testimonials.edit },
40
40
  fields: [
41
41
  ...headFields,
42
42
  {
@@ -35,7 +35,7 @@ export default function testimonialsList(say: Messages): OwnedList {
35
35
  to: 'categories',
36
36
  multiple: true,
37
37
  add: own.addTag,
38
- edit: { to: `ny/${config.lists.tags}`, text: own.entries.createTag },
38
+ edit: { to: `new/${config.lists.tags}`, text: own.entries.createTag },
39
39
  },
40
40
  { name: 'frontPage', label: own.entries.frontPage, type: 'boolean', default: true },
41
41
  ],
package/src/store.ts CHANGED
@@ -1,4 +1,4 @@
1
- // The app store ("Indstillinger" › "Appstore", #indstillinger/appstore, AppList.tsx): the apps that are not in the site
1
+ // The app store ("Indstillinger" › "Appstore", #settings/appstore, AppList.tsx): the apps that are not in the site
2
2
  // and can be installed. The store is reached through the site's Worker (/api/store/catalog, /api/store/files/<oid>;
3
3
  // the Worker holds the site's key to it, and the editor never calls the store itself): each app of its catalog with
4
4
  // its id, its version and the version of the contract it is written to (APP_API), its name, who made it, its icon
@@ -190,10 +190,10 @@ export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
190
190
  /** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
191
191
  * before (STORE_BEFORE, which opens it, App.tsx MOVED), and that of a developer's page (DEVELOPER_ROUTE/<slug>); no
192
192
  * app can have the ids of the last two. */
193
- export const STORE_ROUTE = 'indstillinger/appstore';
194
- export const STORE_THEMES = 'indstillinger/appstore/temaer';
193
+ export const STORE_ROUTE = 'settings/appstore';
194
+ export const STORE_THEMES = 'settings/appstore/themes';
195
195
  export const STORE_BEFORE = 'apps/butik';
196
- export const DEVELOPER_ROUTE = 'apps/udvikler';
196
+ export const DEVELOPER_ROUTE = 'apps/developer';
197
197
 
198
198
  /** An app's folder in the site, and the note in it of where it came from. */
199
199
  export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
package/src/tagUses.d.mts CHANGED
@@ -11,7 +11,7 @@ export function promotionTags(d: Record<string, unknown>): string[];
11
11
  */
12
12
  export function tagUses<F extends TagFile>(files: F[], folders: TagFolders): Map<string, Map<string, F>>;
13
13
  /**
14
- * A file record's use of a tag: the Filer address the file record is for, as a path under filer/.
14
+ * A file record's use of a tag: the Filer address the file record is for, as a path under files/.
15
15
  * @param {TagFile} file
16
16
  * @param {string} folder the file records folder
17
17
  * @returns {{ path: string; name: string } | null}
package/src/tagUses.mjs CHANGED
@@ -87,7 +87,7 @@ export function tagUses(files, folders) {
87
87
  }
88
88
 
89
89
  /**
90
- * A file record's use of a tag: the Filer address the file record is for, as a path under filer/.
90
+ * A file record's use of a tag: the Filer address the file record is for, as a path under files/.
91
91
  * @param {TagFile} file
92
92
  * @param {string} folder the file records folder
93
93
  * @returns {{ path: string; name: string } | null}
@@ -95,7 +95,7 @@ export function tagUses(files, folders) {
95
95
  export function recordUse(file, folder) {
96
96
  const record = folder !== undefined && isRecordPath(file.path, folder) ? parseRecord(file.text) : null;
97
97
  if (!record) return null;
98
- return { path: `filer${record.src}`, name: record.src.slice(record.src.lastIndexOf('/') + 1) };
98
+ return { path: `files${record.src}`, name: record.src.slice(record.src.lastIndexOf('/') + 1) };
99
99
  }
100
100
 
101
101
  /**
@@ -127,7 +127,7 @@ test('taggedWith lists the file records with a tag in their categories', () => {
127
127
 
128
128
  test('recordUse gives the Filer path and the file name of a file record', () => {
129
129
  assert.deepEqual(recordUse(andromeda, 'src/data/files'), {
130
- path: 'filer/images/uploads/2026/10/andromeda.webp',
130
+ path: 'files/images/uploads/2026/10/andromeda.webp',
131
131
  name: 'andromeda.webp',
132
132
  });
133
133
  });
package/src/ui.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  // The editor's parts: every button, field, switch, chip and row a screen is made of, each with one
2
2
  // look, set here. A screen uses these and never writes the classes of a button or a field itself
3
3
  // (CLAUDE.md), so the editor looks the same everywhere and a change here changes it everywhere. They
4
- // are all shown, in every state, on the screen #udseende/dele (Parts.tsx). The screen itself, with its top bar,
4
+ // are all shown, in every state, on the screen #appearance/parts (Parts.tsx). The screen itself, with its top bar,
5
5
  // is Screen.tsx.
6
6
  import {
7
7
  useEffect,
@@ -1096,7 +1096,7 @@ const PAGE_STATES: PageState[] = ['published', 'draft', 'unlisted', 'scheduled']
1096
1096
  * menu links to it and its date. */
1097
1097
  export type Pickable = { value: string; label: string; state?: PageState; inMenu?: boolean; date?: string };
1098
1098
 
1099
- type PickSort = 'nyeste' | 'aeldste' | 'az' | 'za';
1099
+ type PickSort = 'newest' | 'oldest' | 'az' | 'za';
1100
1100
  type InMenu = '' | 'in' | 'out';
1101
1101
 
1102
1102
  /** The order and the filters of a sheet of choices (PagePicker, MultiChoice), as over the lists: the arrows with A–Å and
@@ -1106,7 +1106,7 @@ type InMenu = '' | 'in' | 'out';
1106
1106
  * and `arrange` orders and filters choices. */
1107
1107
  export function usePickTools(all: Pickable[], keep: string) {
1108
1108
  const dated = all.some((o) => o.date);
1109
- const sorts: PickSort[] = dated ? ['nyeste', 'aeldste', 'az', 'za'] : ['az', 'za'];
1109
+ const sorts: PickSort[] = dated ? ['newest', 'oldest', 'az', 'za'] : ['az', 'za'];
1110
1110
  const sortKey = `editor-sort-valg-${keep}`;
1111
1111
  const [savedSort, setSort] = useState<PickSort | null>(() => {
1112
1112
  try {
@@ -1151,8 +1151,8 @@ export function usePickTools(all: Pickable[], keep: string) {
1151
1151
 
1152
1152
  const byDate = (a: Pickable, b: Pickable) => (b.date ?? '').localeCompare(a.date ?? '');
1153
1153
  const compare: Record<PickSort, (a: Pickable, b: Pickable) => number> = {
1154
- nyeste: (a, b) => byDate(a, b) || byText(a.label, b.label),
1155
- aeldste: (a, b) => byDate(b, a) || byText(a.label, b.label),
1154
+ newest: (a, b) => byDate(a, b) || byText(a.label, b.label),
1155
+ oldest: (a, b) => byDate(b, a) || byText(a.label, b.label),
1156
1156
  az: (a, b) => byText(a.label, b.label),
1157
1157
  za: (a, b) => byText(b.label, a.label),
1158
1158
  };
@@ -2740,11 +2740,11 @@ function MainRail({ label, onOpen, children }: { label: string; onOpen: () => vo
2740
2740
  // menu button. The header cell starts at the drawer items' outer edge: the row has no start padding, and the header (the
2741
2741
  // account row, the back row) brings the items' inline padding itself, so its pill (editor.css, .editor-main-drawer-header)
2742
2742
  // has the shape of an item's and the avatar or arrow stands over the items' icons. The button is centred on the 1.5rem box
2743
- // of the items' trailing arrows (a negative end margin from the button's own width). A divider (middle inset) stands under the row.
2743
+ // of the items' trailing arrows (a negative end margin from the button's own width). The row is the top bar's height (`--top-app-bar-height`), so the divider (middle inset) under it stands level with the top bar's line.
2744
2744
  function MainDrawerMenuButton({ onClick, header }: { onClick: () => void; header?: ReactNode }) {
2745
2745
  return (
2746
2746
  <>
2747
- <div className="editor-main-drawer-header mt-2 mb-3 flex items-center gap-1 p-(--navigation-drawer-item-padding,0_1rem) py-0 ps-0">
2747
+ <div className="editor-main-drawer-header flex min-h-(--top-app-bar-height,4rem) items-center gap-1 p-(--navigation-drawer-item-padding,0_1rem) py-0 ps-0">
2748
2748
  <div className="min-w-0 flex-1">{header}</div>
2749
2749
  <MenuButton label={t.library.closeMenu} open onClick={onClick} className="-me-[calc((var(--icon-button-width)-1.5rem)/2)]" />
2750
2750
  </div>
@@ -164,7 +164,7 @@ const jsonLd = {
164
164
  </head>
165
165
  <body class="flex min-h-dvh flex-col">
166
166
  <a
167
- href="#hovedindhold"
167
+ href="#main-content"
168
168
  class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:rounded-full focus:bg-accent focus:px-4 focus:py-2 focus:text-on-accent"
169
169
  >
170
170
  {siteT.theme.skipToContent}
@@ -172,7 +172,7 @@ const jsonLd = {
172
172
 
173
173
  <Header />
174
174
 
175
- <main id="hovedindhold" class="flex-1">
175
+ <main id="main-content" class="flex-1">
176
176
  <slot />
177
177
  </main>
178
178