@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/Consents.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Samtykker" (#privatliv/samtykker, Part.opens 'consents'): the visitors' answers in the cookie banner, as the site's
1
+ // "Consents" (#privacy/consents, Part.opens 'consents'): the visitors' answers in the cookie banner, as the site's
2
2
  // Worker keeps them (consentData.ts, packages/editor/worker/consent.js), newest first, so the site can show that a visitor said yes. Over
3
3
  // the list the search by consent id (the one the banner shows the visitor under "Cookieindstillinger", asked of the
4
4
  // Worker, so older answers are found too) and the order (kept in this browser); in the top bar the download of all of
@@ -32,7 +32,7 @@ import { t } from './i18n';
32
32
 
33
33
  type Sort = 'newest' | 'oldest';
34
34
  const SORTS: Sort[] = ['newest', 'oldest'];
35
- const SORT_KEY = 'editor-sort-samtykker';
35
+ const SORT_KEY = 'editor-sort-consents';
36
36
 
37
37
  // The yes and the no of an answer as one line, in the banner's order: "Ja: Statistik · Nej: Marketing".
38
38
  function summary(answer: ConsentAnswer) {
@@ -46,7 +46,7 @@ function summary(answer: ConsentAnswer) {
46
46
  }
47
47
 
48
48
  export default function Consents({ env, back }: { env: Env; back: { href: string; label: string } }) {
49
- const [query, setQuery] = useKept('samtykker-soeg', '');
49
+ const [query, setQuery] = useKept('consents-search', '');
50
50
  const [sort, setSort] = useState<Sort>(() => {
51
51
  try {
52
52
  const saved = localStorage.getItem(SORT_KEY);
package/src/DataForm.tsx CHANGED
@@ -309,12 +309,12 @@ function Form({
309
309
  title={dataFile ? dataFile.label : isNew ? collection.create! : entryName(folder!, clean(fields, initial)) || kindLabel}
310
310
  back={
311
311
  header.includes(collection.id)
312
- ? { href: '#udseende/header', label: t.common.parts.header }
312
+ ? { href: '#appearance/header', label: t.common.parts.header }
313
313
  : collection.id === 'footer'
314
- ? { href: '#udseende/footer', label: t.common.parts.footer }
314
+ ? { href: '#appearance/footer', label: t.common.parts.footer }
315
315
  : collection.kind === 'files' && collection.files.length === 1
316
316
  ? { href: '#', label: siteName }
317
- : { href: `#liste/${collection.id}`, label: collection.label }
317
+ : { href: `#list/${collection.id}`, label: collection.label }
318
318
  }
319
319
  status={fixed ? savedStatus({ savedAt, saving, deleting }) : undefined}
320
320
  view={view}
package/src/Deleted.tsx CHANGED
@@ -38,7 +38,7 @@ const fileName = (path: string) => path.split('/').pop() ?? path;
38
38
 
39
39
  export default function Deleted({ id, files, env }: { id: string; files: RepoFile[]; env: Env }) {
40
40
  // The files from "Filer" (pictures, PDFs, videos, zip files), not a list's.
41
- const pictures = id === 'filer';
41
+ const pictures = id === 'files';
42
42
  const collection = pictures ? undefined : collections.find((c) => c.id === id && restorable(c));
43
43
  const folder = pictures ? MEDIA : collection && collection.kind !== 'files' ? collection.folder : '';
44
44
  const [deleted, setDeleted] = useState<DeletedFile[] | null>(null);
@@ -62,12 +62,12 @@ export default function Deleted({ id, files, env }: { id: string; files: RepoFil
62
62
  if (!folder)
63
63
  return (
64
64
  <Screen title={t.app.notFound} back={{ href: '#', label: siteName }}>
65
- <EmptyNote>{t.app.nothingAt(`slettet/${id}`)}</EmptyNote>
65
+ <EmptyNote>{t.app.nothingAt(`deleted/${id}`)}</EmptyNote>
66
66
  </Screen>
67
67
  );
68
68
 
69
69
  const label = pictures ? t.history.files : collection!.label;
70
- const back = pictures ? { href: '#filer', label: t.common.parts.media } : { href: `#liste/${id}`, label: collection!.label };
70
+ const back = pictures ? { href: '#files', label: t.common.parts.media } : { href: `#list/${id}`, label: collection!.label };
71
71
 
72
72
  async function open(file: DeletedFile) {
73
73
  if (!pictures) return restore(file);
@@ -1,4 +1,4 @@
1
- // The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#licenser,
1
+ // The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#licenses,
2
2
  // Licenses.tsx). Exposes EditorFooter. Uses LinkButton from ui.tsx; the safe area at the bottom is Screen's main.
3
3
  import { t } from './i18n';
4
4
  import { LinkButton } from './ui';
@@ -6,7 +6,7 @@ import { LinkButton } from './ui';
6
6
  export default function EditorFooter() {
7
7
  return (
8
8
  <footer className="mt-12 flex justify-center">
9
- <LinkButton href="#licenser" kind="text" size="small">
9
+ <LinkButton href="#licenses" kind="text" size="small">
10
10
  {t.common.parts.licenses}
11
11
  </LinkButton>
12
12
  </footer>
package/src/EntryForm.tsx CHANGED
@@ -99,7 +99,7 @@ export const newMarkdown = (collection: MarkdownCollection, off: string[] = []):
99
99
  // The address of a saved page or post: a page's own `path`, or else /<file name>/ (packages/editor/site/content.ts).
100
100
  // '' for a new one.
101
101
  // Where the redirects are, as the menu has it: "Indhold" › "Videresendelser" › "Alle videresendelser".
102
- const redirectsPath = () => [placeOf('videresendelser'), t.common.parts.redirects, t.redirects.all].filter(Boolean).join(' › ');
102
+ const redirectsPath = () => [placeOf('redirects'), t.common.parts.redirects, t.redirects.all].filter(Boolean).join(' › ');
103
103
 
104
104
  function addressNow(collection: MarkdownCollection, file: RepoFile, data: Record<string, unknown>) {
105
105
  if (!file.path) return '';
@@ -623,7 +623,7 @@ function Form({
623
623
  <WrongField.Provider value={wrongField.field}>
624
624
  <FormFrame
625
625
  title={formTitle}
626
- back={{ href: `#liste/${collection.id}`, label: collection.label }}
626
+ back={{ href: `#list/${collection.id}`, label: collection.label }}
627
627
  status={savedStatus({ savedAt, saving, deleting, isNew })}
628
628
  view={isNew ? undefined : url}
629
629
  dirty={dirty}
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Footer" (#udseende/footer): the footer's text under the name, its two headings and its links (footer.json),
1
+ // "Udseende" › "Footer" (#appearance/footer): the footer's text under the name, its two headings and its links (footer.json),
2
2
  // on one screen laid out as a page's form (SettingsSections.tsx): a preview of the footer as the form has it, on a phone and
3
3
  // a computer (partPreview.tsx), over the sections Text, Contact and Links. "Gem ændringer" saves the file in its own
4
4
  // commit (useDataFiles.ts). Exposes FooterSettings (default). Uses fields.tsx and footerPreview.mjs.
@@ -30,7 +30,7 @@ const placeOf = (path: string) => places.find((p) => p.names.includes(path.split
30
30
  // The class of a link in the footer (Footer.astro), for a footer whose page has no link to copy.
31
31
  const THEME = { linkClass: 'md-footer-item' };
32
32
 
33
- const back = { href: '#udseende', label: t.common.parts.appearance };
33
+ const back = { href: '#appearance', label: t.common.parts.appearance };
34
34
 
35
35
  export default function FooterSettings({ env }: { env: Env }) {
36
36
  const layout = useSettingsSections(places);
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Header" (#udseende/header): what is in the header on every page, on one screen laid out as a page's form
1
+ // "Udseende" › "Header" (#appearance/header): what is in the header on every page, on one screen laid out as a page's form
2
2
  // (SettingsSections.tsx): a preview of the header as the form has it, on a phone and a computer (partPreview.tsx), over the
3
3
  // sections Main menu (menu.json), Mobile menu (mobile-menu.json), Action button and Menu button (header.json). "Gem
4
4
  // ændringer" saves each changed file in its own commit (useDataFiles.ts). Exposes HeaderSettings (default). Uses
@@ -19,7 +19,7 @@ import type { Env } from './App';
19
19
  import { t } from './i18n';
20
20
 
21
21
  // The header's three files, by their collections (defaults/collections.ts), as the preview names them.
22
- const FILES = { menu: 'menu', mobileMenu: 'mobilmenu', header: 'headerknap' } as const;
22
+ const FILES = { menu: 'menu', mobileMenu: 'mobile-menu', header: 'header-button' } as const;
23
23
  const parts: DataFilePart[] = Object.values(FILES).flatMap((id) => {
24
24
  const collection = collections.find((c): c is FilesCollection => c.id === id && c.kind === 'files');
25
25
  return collection ? [{ collection, dataFile: collection.files[0] }] : [];
@@ -51,7 +51,7 @@ const shapeOf = (values: Values) => ({
51
51
  header: values[at(FILES.header)] ?? {},
52
52
  });
53
53
 
54
- const back = { href: '#udseende', label: t.common.parts.appearance };
54
+ const back = { href: '#appearance', label: t.common.parts.appearance };
55
55
 
56
56
  export default function HeaderSettings({ env }: { env: Env }) {
57
57
  const layout = useSettingsSections(places);
package/src/Keywords.tsx CHANGED
@@ -26,7 +26,7 @@ type Keyword = { word: string; primary: boolean; title: string; href: string; ad
26
26
  type Counts = Record<string, { href: string; pages: number }>;
27
27
 
28
28
  type Sort = 'az' | 'za' | 'most' | 'fewest';
29
- const SORT_KEY = 'editor-sort-soegeord';
29
+ const SORT_KEY = 'editor-sort-keywords';
30
30
  function savedSort(): Sort {
31
31
  try {
32
32
  const s = localStorage.getItem(SORT_KEY);
@@ -85,11 +85,11 @@ function keywordsOf(files: RepoFile[]): Keyword[] {
85
85
  }
86
86
 
87
87
  export default function Keywords({ files }: { files: RepoFile[] }) {
88
- const [query, setQuery] = useKept('soegeord-soeg', '');
88
+ const [query, setQuery] = useKept('keywords-search', '');
89
89
  const [sort, setSort] = useState(savedSort);
90
- const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('soegeord-slags', '');
91
- const [linked, setLinked] = useKept<'' | 'with' | 'without'>('soegeord-links', '');
92
- const [kind, setKind] = useKept('soegeord-side', '');
90
+ const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('keywords-kind', '');
91
+ const [linked, setLinked] = useKept<'' | 'with' | 'without'>('keywords-links', '');
92
+ const [kind, setKind] = useKept('keywords-page', '');
93
93
  // undefined while they load; null when the site has none (not built since they came, or offline).
94
94
  const [counts, setCounts] = useState<Counts | null>();
95
95
  useEffect(() => {
package/src/Library.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  // and posts can be filtered by their status and by what they pick (a post's tags), a list of what is
6
6
  // shown on pages (Promovering) by a page, its kind and whether it is on, as "Filer" (Filters). In a list that can be deleted from, the clock in the top bar ("Slettede …") opens
7
7
  // what was deleted (Deleted.tsx), as in "Filer". The tabs the apps installed add to a list (AppDefinition.tabs, Cookiescanner's
8
- // "Problemer" on "Cookies") are in its top app bar after the list's own, each at #liste/<list>/<app>. "Use theme" and the move arrows show the
8
+ // "Problemer" on "Cookies") are in its top app bar after the list's own, each at #list/<list>/<app>. "Use theme" and the move arrows show the
9
9
  // loading indicator while they run (only the pressed one).
10
10
  import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react';
11
11
  import {
@@ -116,7 +116,7 @@ import { offBlocks } from './registry';
116
116
  import { sideChoicesOf, sideSettingOf } from './SideColumn';
117
117
 
118
118
  // Where a part opens: its list, or the file itself when it is one file (the menu).
119
- export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
119
+ export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#list/${c.id}`);
120
120
 
121
121
  const countOf = (c: Collection, files: RepoFile[]): number =>
122
122
  c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
@@ -124,39 +124,39 @@ const countOf = (c: Collection, files: RepoFile[]): number =>
124
124
  // A part's name, where it opens, and how many entries it has (null for one file): for the main drawer (MainMenu.tsx).
125
125
  export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
126
126
  const { parts } = t.common;
127
- if (id === 'virksomheden') return { label: parts.company, href: '#virksomheden', count: null };
128
- if (id === 'header') return { label: parts.header, href: '#udseende/header', count: null };
129
- if (id === 'footer') return { label: parts.footer, href: '#udseende/footer', count: null };
130
- if (id === 'lister') return { label: parts.lists, href: '#lister', count: listsShown(files).length };
131
- if (id === 'blokke') return { label: parts.blocks, href: '#blokke', count: blockParts.length };
132
- if (id === 'filer') return { label: parts.media, href: '#filer', count: mediaOf(files).length + filesOf(files).length };
133
- if (id === 'udseende') return { label: parts.appearance, href: '#udseende', count: null };
134
- if (id === 'blokvalg') return { label: parts.blockSettings, href: '#blokvalg', count: null };
127
+ if (id === 'company') return { label: parts.company, href: '#company', count: null };
128
+ if (id === 'header') return { label: parts.header, href: '#appearance/header', count: null };
129
+ if (id === 'footer') return { label: parts.footer, href: '#appearance/footer', count: null };
130
+ if (id === 'lists') return { label: parts.lists, href: '#lists', count: listsShown(files).length };
131
+ if (id === 'blocks') return { label: parts.blocks, href: '#blocks', count: blockParts.length };
132
+ if (id === 'files') return { label: parts.media, href: '#files', count: mediaOf(files).length + filesOf(files).length };
133
+ if (id === 'appearance') return { label: parts.appearance, href: '#appearance', count: null };
134
+ if (id === 'block-settings') return { label: parts.blockSettings, href: '#block-settings', count: null };
135
135
  if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
136
136
  if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
137
- if (id === 'videresendelser') return { label: parts.redirects, href: `#${REDIRECTS_ROUTE}`, count: null };
137
+ if (id === 'redirects') return { label: parts.redirects, href: `#${REDIRECTS_ROUTE}`, count: null };
138
138
  const seoPart = seoParts.find((p) => p.id === id);
139
139
  if (seoPart) return { label: seoPart.label, href: `#seo/${id}`, count: null };
140
140
  // The screens under "Udseende" (Theme): the ones the editor has, then the site's own (appearance).
141
141
  const appearanceLabel = {
142
- temaer: parts.themes,
143
- ikonpakker: parts.iconPacks,
144
- bevaegelse: parts.motion,
145
- sidekolonne: parts.sideColumn,
146
- farver: parts.colors,
142
+ themes: parts.themes,
143
+ 'icon-packs': parts.iconPacks,
144
+ motion: parts.motion,
145
+ 'side-column': parts.sideColumn,
146
+ colors: parts.colors,
147
147
  logo: parts.brand,
148
- dele: parts.editorParts,
148
+ parts: parts.editorParts,
149
149
  }[id];
150
150
  const appearancePart = appearanceLabel ?? appearance.find((p) => p.id === id)?.label;
151
- if (appearancePart) return { label: appearancePart, href: `#udseende/${id}`, count: null };
151
+ if (appearancePart) return { label: appearancePart, href: `#appearance/${id}`, count: null };
152
152
  // The parts of a screen with parts ("Privatliv"): where each opens, or its screen's address with its id.
153
153
  for (const screen of partScreens) {
154
154
  const own = screen.parts.find((p) => p.id === id);
155
155
  if (own) return { label: own.label, href: own.href ?? `#${screen.id}/${id}`, count: null };
156
156
  }
157
157
  if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
158
- if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
159
- if (id === 'licenser') return { label: parts.licenses, href: '#licenser', count: null };
158
+ if (id === 'advanced') return { label: parts.advanced, href: '#advanced', count: null };
159
+ if (id === 'licenses') return { label: parts.licenses, href: '#licenses', count: null };
160
160
  const screen = partScreens.find((s) => s.id === id);
161
161
  if (screen) return { label: screen.label, href: `#${id}`, count: null };
162
162
  if (id === 'seo') return { label: parts.seo, href: '#seo', count: null };
@@ -199,12 +199,12 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
199
199
  return (
200
200
  <Parts
201
201
  title={part.label}
202
- route={`virksomheden/${part.id}`}
202
+ route={`company/${part.id}`}
203
203
  parts={[...part.parts, ...appPartsIn(part, files)]}
204
- back={{ href: '#virksomheden', label: t.common.parts.company }}
204
+ back={{ href: '#company', label: t.common.parts.company }}
205
205
  />
206
206
  );
207
- return <Parts title={t.common.parts.company} route="virksomheden" parts={companyParts} />;
207
+ return <Parts title={t.common.parts.company} route="company" parts={companyParts} />;
208
208
  }
209
209
 
210
210
  // "Udseende": first "Temaer" (Themes), with the theme the site is built with at the end of its row (or, after a swap,
@@ -218,28 +218,28 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
218
218
  export function Theme({ files }: { files: RepoFile[] }) {
219
219
  const chosen = themes.find((x) => x.id === chosenTheme(files)) ?? theme;
220
220
  const parts: Part[] = [
221
- { id: 'temaer', label: t.common.parts.themes, icon: icons.temaer, detail: chosen.name },
221
+ { id: 'themes', label: t.common.parts.themes, icon: icons.themes, detail: chosen.name },
222
222
  {
223
- id: 'ikonpakker',
223
+ id: 'icon-packs',
224
224
  label: t.common.parts.iconPacks,
225
- icon: icons.ikonpakker,
225
+ icon: icons['icon-packs'],
226
226
  detail: iconPacks.find((x) => x.id === chosenPack(files))?.name,
227
227
  },
228
- { id: 'bevaegelse', label: t.common.parts.motion, icon: icons.bevaegelse, detail: t.library.motionSchemes[motionOf(files)] },
228
+ { id: 'motion', label: t.common.parts.motion, icon: icons.motion, detail: t.library.motionSchemes[motionOf(files)] },
229
229
  {
230
- id: 'sidekolonne',
230
+ id: 'side-column',
231
231
  label: t.common.parts.sideColumn,
232
- icon: icons.sidekolonne,
232
+ icon: icons['side-column'],
233
233
  detail: sideChoicesOf(files).find((x) => x.type === sideSettingOf(files))?.label ?? t.library.sideNone,
234
234
  },
235
- { id: 'farver', label: t.common.parts.colors, icon: icons.farver, trailing: <ThemeColors files={files} /> },
235
+ { id: 'colors', label: t.common.parts.colors, icon: icons.colors, trailing: <ThemeColors files={files} /> },
236
236
  { id: 'logo', label: t.common.parts.brand, icon: icons.logo },
237
237
  { id: 'header', label: t.common.parts.header, icon: icons.header },
238
238
  { id: 'footer', label: t.common.parts.footer, icon: icons.footer },
239
239
  ...appearance,
240
- { id: 'dele', label: t.common.parts.editorParts, icon: icons.dele },
240
+ { id: 'parts', label: t.common.parts.editorParts, icon: icons.parts },
241
241
  ];
242
- return <Parts title={t.common.parts.appearance} route="udseende" parts={parts} back={backFrom('udseende')} />;
242
+ return <Parts title={t.common.parts.appearance} route="appearance" parts={parts} back={backFrom('appearance')} />;
243
243
  }
244
244
 
245
245
  /** By "Farver": the site's primary, secondary and tertiary colours (accent, secondary, tertiary) in the mode the editor
@@ -252,8 +252,8 @@ function ThemeColors({ files }: { files: RepoFile[] }) {
252
252
  );
253
253
  }
254
254
 
255
- const backToAppearance = { href: '#udseende', label: t.common.parts.appearance };
256
- const backToThemes = { href: '#udseende/temaer', label: t.common.parts.themes };
255
+ const backToAppearance = { href: '#appearance', label: t.common.parts.appearance };
256
+ const backToThemes = { href: '#appearance/themes', label: t.common.parts.themes };
257
257
 
258
258
  /** Under a theme's name: whether the site is built with it ("I brug") or is changing to it ("Skifter hertil"). */
259
259
  function themeStatus(id: string, files: RepoFile[]): string | undefined {
@@ -261,7 +261,7 @@ function themeStatus(id: string, files: RepoFile[]): string | undefined {
261
261
  if (id === chosenTheme(files)) return t.library.themeNext;
262
262
  }
263
263
 
264
- /** "Temaer" (#udseende/temaer): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
264
+ /** "Temaer" (#appearance/themes): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
265
265
  * does not have yet, as "Installeres…"), by name, each with its picture, its version and who made it, and whether it is
266
266
  * in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
267
267
  export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
@@ -281,7 +281,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
281
281
  return (
282
282
  <ListItem
283
283
  key={x.id}
284
- href={`#udseende/temaer/${x.id}`}
284
+ href={`#appearance/themes/${x.id}`}
285
285
  leading={<ThemePicture src={x.icon} />}
286
286
  headline={x.name}
287
287
  supporting={t.library.themeByline(x.version, x.author)}
@@ -296,7 +296,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
296
296
  );
297
297
  }
298
298
 
299
- /** "Ikonpakker" (#udseende/ikonpakker): the icon packs installed (the system's own, packages/editor/icons/, and those the themes
299
+ /** "Ikonpakker" (#appearance/icon-packs): the icon packs installed (the system's own, packages/editor/icons/, and those the themes
300
300
  * installed bring; iconPacks.ts), by name, each with who made it, its licence, the theme it came with and a few of its
301
301
  * icons as they are, the one chosen ticked (chosenPack), with "I brug" by the one the site and the editor are built with
302
302
  * and "Skifter hertil …" by the one chosen until they are. A tap on another asks first and then saves it in the site's
@@ -365,7 +365,7 @@ export function motionOf(files: RepoFile[]): MotionScheme {
365
365
  return schemeInUse(scheme, theme.motion, themes.find((x) => x.id === STANDARD_THEME)?.motion);
366
366
  }
367
367
 
368
- /** "Bevægelse" (#udseende/bevaegelse): Material 3's motion schemes and our calm one (lib/motion.ts), each with how it
368
+ /** "Bevægelse" (#appearance/motion): Material 3's motion schemes and our calm one (lib/motion.ts), each with how it
369
369
  * moves, the one in use ticked (motionOf) and "Temaets valg" by the theme's own. A tap on another asks first and then
370
370
  * saves it in the site's file (config.motion) at once, in its own commit, as an icon pack is; the editor moves by it at
371
371
  * once (applyMotion, App.tsx), the site after it is built. */
@@ -422,7 +422,7 @@ export function MotionSchemes({ env, files }: { env: Env; files: RepoFile[] }) {
422
422
  );
423
423
  }
424
424
 
425
- /** A theme's page (#udseende/temaer/<id>), the same for every theme, made to show what it would make of the site, so
425
+ /** A theme's page (#appearance/themes/<id>), the same for every theme, made to show what it would make of the site, so
426
426
  * it can be chosen or not: its picture, name, version and who made it, and whether it is in use; "Brug temaet", for any
427
427
  * other than the one chosen, which asks first and then saves it in the site's file (config.theme) at once, in its own
428
428
  * commit, and the site is built with it; the pictures of the site in it (its screenshots/, packages/editor/scripts/theme-screenshots.mjs),
@@ -505,37 +505,37 @@ export function ThemePage({ of, env, files }: { of: ThemeInfo; env: Env; files:
505
505
  <ListItem key={text} icon={glyph} headline={text} />
506
506
  ))}
507
507
  </Group>
508
- <OwnerLicenses id={`tema/${of.id}`} name={of.name} author={of.author} version={of.version} declared={of} />
508
+ <OwnerLicenses id={`theme/${of.id}`} name={of.name} author={of.author} version={of.version} declared={of} />
509
509
  <DeveloperRow name={of.author} />
510
510
  <ThemeStoreButtons of={of} env={env} files={files} />
511
511
  </Screen>
512
512
  );
513
513
  }
514
514
 
515
- /** "Dele" (#udseende/dele): the parts the editor and the site are built of, each on a screen of its own: the editor's
515
+ /** "Dele" (#appearance/parts): the parts the editor and the site are built of, each on a screen of its own: the editor's
516
516
  * (Parts.tsx), the theme's UI components, the blocks (the standard ones and the apps') and each list of kinds
517
517
  * ("Promovering", its own kinds and the apps'), as the theme shows them (BlockParts.tsx), and the theme's design tokens ("Værdier", Tokens.tsx). */
518
518
  export function PartsHome() {
519
519
  const parts: Part[] = [
520
- { id: 'editor', label: t.parts.screens.editor, icon: icons.dele },
521
- { id: 'ui', label: t.parts.screens.components, icon: icons.udseende },
522
- { id: 'blokke', label: t.parts.screens.blocks, icon: icons.blokke },
523
- ...kindLists().map((c) => ({ id: `liste/${c.id}`, label: c.label, icon: icons[c.id] ?? icons.lister })),
524
- { id: 'vaerdier', label: t.parts.screens.values, icon: Ruler },
520
+ { id: 'editor', label: t.parts.screens.editor, icon: icons.parts },
521
+ { id: 'ui', label: t.parts.screens.components, icon: icons.appearance },
522
+ { id: 'blocks', label: t.parts.screens.blocks, icon: icons.blocks },
523
+ ...kindLists().map((c) => ({ id: `list/${c.id}`, label: c.label, icon: icons[c.id] ?? icons.lists })),
524
+ { id: 'values', label: t.parts.screens.values, icon: Ruler },
525
525
  ];
526
- return <Parts title={t.common.parts.editorParts} route="udseende/dele" parts={parts} back={backToAppearance} />;
526
+ return <Parts title={t.common.parts.editorParts} route="appearance/parts" parts={parts} back={backToAppearance} />;
527
527
  }
528
528
 
529
- /** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/videresendelser) and then
529
+ /** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/redirects) and then
530
530
  * the site's own parts (seoParts: "Interne links"), each on a screen of its own (CompanyForm.tsx). "SEO-vurdering"
531
- * (SeoScores.tsx, #seo/vurdering) is hidden: nothing links to it. */
531
+ * (SeoScores.tsx, #seo/rating) is hidden: nothing links to it. */
532
532
  export function Seo() {
533
- const parts: Part[] = [{ id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser }, ...seoParts];
533
+ const parts: Part[] = [{ id: 'redirects', label: t.common.parts.redirects, icon: icons.redirects }, ...seoParts];
534
534
  return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
535
535
  }
536
536
 
537
537
  // A screen with parts of the site's own (partScreens): "Privatliv"; and with the settings of the apps installed with its
538
- // category (PartScreen.apps): "Analytics" and "Promovering", each opening the app's form (#maaling/google-analytics).
538
+ // category (PartScreen.apps): "Analytics" and "Promovering", each opening the app's form (#analytics/google-analytics).
539
539
  export function PartsScreen({ screen, files }: { screen: PartScreen; files: RepoFile[] }) {
540
540
  const parts = [...screen.parts, ...appPartsIn(screen, files)];
541
541
  return (
@@ -551,11 +551,11 @@ export function PartsScreen({ screen, files }: { screen: PartScreen; files: Repo
551
551
  // and the funnel with the filters by group and by whether the block is on a page, each picked one as a chip.
552
552
  type BlockSort = 'group' | 'az' | 'za';
553
553
  const BLOCK_SORTS: BlockSort[] = ['group', 'az', 'za'];
554
- const BLOCK_SORT_KEY = 'editor-sort-blokke';
554
+ const BLOCK_SORT_KEY = 'editor-sort-blocks';
555
555
  type Use = '' | 'used' | 'unused';
556
556
 
557
557
  export function Blocks({ files }: { files: RepoFile[] }) {
558
- const [query, setQuery] = useKept('blokke-soeg', '');
558
+ const [query, setQuery] = useKept('blocks-search', '');
559
559
  const [sort, setSort] = useState<BlockSort>(() => {
560
560
  try {
561
561
  const saved = localStorage.getItem(BLOCK_SORT_KEY);
@@ -563,8 +563,8 @@ export function Blocks({ files }: { files: RepoFile[] }) {
563
563
  } catch {}
564
564
  return 'group';
565
565
  });
566
- const [group, setGroup] = useKept('blokke-gruppe', '');
567
- const [use, setUse] = useKept<Use>('blokke-brug', '');
566
+ const [group, setGroup] = useKept('blocks-group', '');
567
+ const [use, setUse] = useKept<Use>('blocks-use', '');
568
568
  // The blocks on a page (any copy, also a hidden one), by their type (a part's id).
569
569
  const used = useMemo(() => new Set(blockParts.filter((p) => pagesWith(p.id, files).length).map((p) => p.id)), [files]);
570
570
  const bothUses = blockParts.some((p) => used.has(p.id)) && blockParts.some((p) => !used.has(p.id));
@@ -648,9 +648,9 @@ export function Blocks({ files }: { files: RepoFile[] }) {
648
648
  {!inGroups.length ? (
649
649
  <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
650
650
  ) : sort === 'group' ? (
651
- inGroups.map((g) => <PartGroup key={g.title} title={g.title} route="blokke" parts={g.parts} />)
651
+ inGroups.map((g) => <PartGroup key={g.title} title={g.title} route="blocks" parts={g.parts} />)
652
652
  ) : (
653
- lettered.map((g) => <PartGroup key={g.key ?? 'all'} title={g.key ?? undefined} route="blokke" parts={g.items} />)
653
+ lettered.map((g) => <PartGroup key={g.key ?? 'all'} title={g.key ?? undefined} route="blocks" parts={g.items} />)
654
654
  )}
655
655
  </Screen>
656
656
  );
@@ -973,7 +973,7 @@ type View = 'liste' | 'menu';
973
973
  const seoKey = (c: Collection) => `editor-seo-${c.id}`;
974
974
  function savedSeo(c: Collection): boolean {
975
975
  try {
976
- return localStorage.getItem(seoKey(c)) === 'vis';
976
+ return localStorage.getItem(seoKey(c)) === 'show';
977
977
  } catch {
978
978
  return false;
979
979
  }
@@ -1004,16 +1004,16 @@ const saved = new Map<string, { oid: string; date: Date }>();
1004
1004
 
1005
1005
  // The ways a list can be sorted; by time only where the entries have one: blog posts by their date,
1006
1006
  // pages by when they were last saved; by their kind where they have kinds (Promovering).
1007
- type Sort = 'nyeste' | 'aeldste' | 'az' | 'za' | 'type';
1007
+ type Sort = 'newest' | 'oldest' | 'az' | 'za' | 'type';
1008
1008
  const sortLabel = (c: Collection, s: Sort) => {
1009
1009
  const { sorts } = t.library;
1010
- return c.id === 'pages' && s === 'nyeste' ? sorts.savedNewest : c.id === 'pages' && s === 'aeldste' ? sorts.savedOldest : sorts[s];
1010
+ return c.id === 'pages' && s === 'newest' ? sorts.savedNewest : c.id === 'pages' && s === 'oldest' ? sorts.savedOldest : sorts[s];
1011
1011
  };
1012
1012
 
1013
1013
  const byTitle = (a: Item, b: Item) => byText(a.title, b.title);
1014
1014
  const compare: Record<Sort, (a: Item, b: Item) => number> = {
1015
- nyeste: (a, b) => (b.date ?? '').localeCompare(a.date ?? '') || byTitle(a, b),
1016
- aeldste: (a, b) => (a.date ?? '').localeCompare(b.date ?? '') || byTitle(a, b),
1015
+ newest: (a, b) => (b.date ?? '').localeCompare(a.date ?? '') || byTitle(a, b),
1016
+ oldest: (a, b) => (a.date ?? '').localeCompare(b.date ?? '') || byTitle(a, b),
1017
1017
  az: byTitle,
1018
1018
  za: (a, b) => byTitle(b, a),
1019
1019
  type: (a, b) => byText(a.kind ?? '', b.kind ?? '') || byTitle(a, b),
@@ -1039,7 +1039,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1039
1039
  // The tabs the apps installed add to it, and the one open (none: the list's own).
1040
1040
  const appTabs = tabsOf(collection.id, apps, installedApps(files));
1041
1041
  const appTab = appTabs.find((a) => a.app.id === tab);
1042
- const [query, setQuery] = useKept(`${collection.id}-soeg`, '');
1042
+ const [query, setQuery] = useKept(`${collection.id}-search`, '');
1043
1043
  // The move that runs (row and target row, null: none); only its arrow shows busy, all arrows stay disabled meanwhile.
1044
1044
  const [moving, setMoving] = useState<{ from: number; to: number } | null>(null);
1045
1045
  const busy = moving !== null;
@@ -1058,7 +1058,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1058
1058
  collection.kind === 'files' || (collection.kind === 'folder' && collection.orderable) || items.length < 2
1059
1059
  ? []
1060
1060
  : collection.id === 'blog' || pages
1061
- ? ['nyeste', 'aeldste', 'az', 'za']
1061
+ ? ['newest', 'oldest', 'az', 'za']
1062
1062
  : collection.kind === 'folder' && collection.kinds
1063
1063
  ? ['az', 'za', 'type']
1064
1064
  : ['az', 'za'];
@@ -1099,7 +1099,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1099
1099
  // A list of what is shown on pages: only those on one page or the posts of one tag (and those on every
1100
1100
  // page), picked among the pages and tags the entries are on, by their names (relationsOf), the tags
1101
1101
  // under a heading of their own (`tag:<slug>`); all again when that one is no longer among them.
1102
- const [onPage, setOnPage] = useKept(`${collection.id}-side`, '');
1102
+ const [onPage, setOnPage] = useKept(`${collection.id}-page`, '');
1103
1103
  const pageNames = useMemo(() => {
1104
1104
  if (!items.some((i) => i.shownOn)) return [];
1105
1105
  const { pages: all, fixed, categories } = relationsOf(files);
@@ -1247,7 +1247,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1247
1247
  function pickSeo(next: boolean) {
1248
1248
  setSeo(next);
1249
1249
  try {
1250
- localStorage.setItem(seoKey(collection), next ? 'vis' : 'skjul');
1250
+ localStorage.setItem(seoKey(collection), next ? 'show' : 'hide');
1251
1251
  } catch {}
1252
1252
  }
1253
1253
 
@@ -1334,7 +1334,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1334
1334
  />
1335
1335
  );
1336
1336
  };
1337
- const privacyScreen = partScreens.find((s) => s.id === 'privatliv');
1337
+ const privacyScreen = partScreens.find((s) => s.id === 'privacy');
1338
1338
  return (
1339
1339
  <Screen
1340
1340
  title={collection.label}
@@ -1343,9 +1343,9 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1343
1343
  collection.id === config.lists.cookies && privacyScreen
1344
1344
  ? { href: `#${privacyScreen.id}`, label: privacyScreen.label }
1345
1345
  : lists.includes(collection.id)
1346
- ? { href: '#lister', label: t.common.parts.lists }
1346
+ ? { href: '#lists', label: t.common.parts.lists }
1347
1347
  : header.includes(collection.id)
1348
- ? { href: '#udseende/header', label: t.common.parts.header }
1348
+ ? { href: '#appearance/header', label: t.common.parts.header }
1349
1349
  : { href: '#', label: siteName }
1350
1350
  }
1351
1351
  tabs={
@@ -1357,7 +1357,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1357
1357
  { value: '', label: collection.label, icon: icons[collection.id as keyof typeof icons] ?? ListIcon },
1358
1358
  ...appTabs.map((a) => ({ value: a.app.id, label: siteText(a.tab.label), icon: a.tab.icon })),
1359
1359
  ],
1360
- onChange: (v) => (location.hash = v ? `liste/${collection.id}/${v}` : `liste/${collection.id}`),
1360
+ onChange: (v) => (location.hash = v ? `list/${collection.id}/${v}` : `list/${collection.id}`),
1361
1361
  }
1362
1362
  : undefined
1363
1363
  }
@@ -1369,7 +1369,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1369
1369
  icon={ClockCounterClockwise}
1370
1370
  label={t.history.deletedOf(collection.label)}
1371
1371
 
1372
- onClick={() => (location.hash = `slettet/${collection.id}`)}
1372
+ onClick={() => (location.hash = `deleted/${collection.id}`)}
1373
1373
  />
1374
1374
  )}
1375
1375
  </>
package/src/Licenses.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Licenser" (#licenser), under "Indstillinger": the third-party work the site and the editor are made with, as the
1
+ // "Licenser" (#licenses), under "Indstillinger": the third-party work the site and the editor are made with, as the
2
2
  // site was last built (/editor/licenses.json, licensesIntegration in packages/editor/lib/licenses.mjs), with a search by
3
3
  // name, who made it and the licence's name. First the icon packs (the system's, each with the sets it takes icons from
4
4
  // besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded,
@@ -6,7 +6,7 @@
6
6
  // then each theme the site is built with and each app installed, under its name: its own licence first, then what it
7
7
  // brings (what it declares, its icon packs and what is shipped through them alone). Each row has the work's name, its
8
8
  // version, licence and who made it, and what it is and where it is used; one with a text opens it
9
- // (#licenser/<id>, LicensePage). An app's and a theme's page has its group too (OwnerLicenses), and one in the store,
9
+ // (#licenses/<id>, LicensePage). An app's and a theme's page has its group too (OwnerLicenses), and one in the store,
10
10
  // from the catalog, the same without the texts (StoreLicenseRows).
11
11
  import { useEffect, useState } from 'react';
12
12
  import { Certificate, Code, Image, Shapes, TextAa, type Icon } from '@phosphor-icons/react';
@@ -32,7 +32,7 @@ import {
32
32
  type ThirdParty,
33
33
  } from './licenseData';
34
34
 
35
- export const LICENSES_ROUTE = 'licenser';
35
+ export const LICENSES_ROUTE = 'licenses';
36
36
 
37
37
  const KIND_ICONS: Record<LicenseKind, Icon> = { font: TextAa, icons: Shapes, library: Code, image: Image };
38
38
 
@@ -46,7 +46,7 @@ const ownerName = (o: Pick<LicenseOwner, 'kind' | 'key' | 'name'>) =>
46
46
  /** Who brings a work, as a sentence's end: "i editoren og på sitet", "i temaet Material". */
47
47
  function placesOf(where: LicenseWhere[], owners: LicenseOwner[]) {
48
48
  // The editor and the site first, then the themes and the apps.
49
- const rank = (w: LicenseWhere) => (w === 'editor' ? 0 : w === 'site' ? 1 : w.startsWith('tema/') ? 2 : 3);
49
+ const rank = (w: LicenseWhere) => (w === 'editor' ? 0 : w === 'site' ? 1 : w.startsWith('theme/') ? 2 : 3);
50
50
  const named = [...where]
51
51
  .sort((a, b) => rank(a) - rank(b))
52
52
  .map((w) => {
@@ -159,7 +159,7 @@ const worksOf = (owner: LicenseOwner, file: LicensesFile) =>
159
159
 
160
160
  export default function Licenses() {
161
161
  const file = useLicenses();
162
- const [query, setQuery] = useKept('licenser-soeg', '');
162
+ const [query, setQuery] = useKept('licenses-search', '');
163
163
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
164
164
  const matches = (...texts: (string | null | undefined)[]) => {
165
165
  const all = texts.filter(Boolean).join(' ').toLowerCase();
@@ -170,7 +170,7 @@ export default function Licenses() {
170
170
 
171
171
  const owners = file?.owners ?? [];
172
172
  // The system's icon packs (a theme's own are under it, `where` it).
173
- const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('tema/')))) : [];
173
+ const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('theme/')))) : [];
174
174
  // What the editor and the site bring: the CMS's own licence first, the rest A–Å.
175
175
  const { cms, rest: system } = splitCms(
176
176
  file ? sorted(file.works.filter((w) => w.kind !== 'icons' && w.where.some((x) => x === 'editor' || x === 'site'))) : [],
@@ -233,7 +233,7 @@ const paragraphsOf = (text: string) =>
233
233
  )
234
234
  .filter(Boolean);
235
235
 
236
- /** One licence (#licenser/<id>): a work's, or an app's or a theme's own: its name, licence, who made it, what it is and
236
+ /** One licence (#licenses/<id>): a work's, or an app's or a theme's own: its name, licence, who made it, what it is and
237
237
  * its version, and its text. */
238
238
  export function LicensePage({ id }: { id: string }) {
239
239
  const file = useLicenses();
@@ -295,7 +295,7 @@ export function OwnerLicenses({
295
295
  }) {
296
296
  const file = useLicenses();
297
297
  const owner = file?.owners.find((o) => o.id === id);
298
- const [kind, key] = id.split('/') as ['app' | 'tema', string];
298
+ const [kind, key] = id.split('/') as ['app' | 'theme', string];
299
299
  const self = owner ?? {
300
300
  id,
301
301
  kind: kind === 'app' ? ('app' as const) : ('theme' as const),