@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/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 {
@@ -21,7 +21,6 @@ import {
21
21
  ListBullets,
22
22
  Moon,
23
23
  Palette,
24
- Plus,
25
24
  Ruler,
26
25
  Shapes,
27
26
  SignOut,
@@ -33,6 +32,7 @@ import { config, countsOf, header, relationsOf, siteName } from '@site/editor';
33
32
  import { iconPack, iconPacks } from './iconPacks';
34
33
  import { theme, themes, type ThemeInfo } from './themes';
35
34
  import { StartFab } from './StartFab';
35
+ import { ListFab } from './ListFab';
36
36
  import { StartStatus } from './StartStatus';
37
37
  import {
38
38
  appearance,
@@ -60,14 +60,12 @@ import {
60
60
  Group,
61
61
  IconButton,
62
62
  LeadingIcon,
63
- NewChoice,
64
63
  SearchRow,
65
64
  useKept,
66
65
  SegmentedButton,
67
66
  UnfoldButton,
68
67
  GroupTitle,
69
68
  SwitchRow,
70
- IconLink,
71
69
  ScoreRing,
72
70
  scoreOf,
73
71
  Button,
@@ -118,7 +116,7 @@ import { offBlocks } from './registry';
118
116
  import { sideChoicesOf, sideSettingOf } from './SideColumn';
119
117
 
120
118
  // Where a part opens: its list, or the file itself when it is one file (the menu).
121
- 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}`);
122
120
 
123
121
  const countOf = (c: Collection, files: RepoFile[]): number =>
124
122
  c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
@@ -126,39 +124,39 @@ const countOf = (c: Collection, files: RepoFile[]): number =>
126
124
  // A part's name, where it opens, and how many entries it has (null for one file): for the main drawer (MainMenu.tsx).
127
125
  export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
128
126
  const { parts } = t.common;
129
- if (id === 'virksomheden') return { label: parts.company, href: '#virksomheden', count: null };
130
- if (id === 'header') return { label: parts.header, href: '#udseende/header', count: null };
131
- if (id === 'footer') return { label: parts.footer, href: '#udseende/footer', count: null };
132
- if (id === 'lister') return { label: parts.lists, href: '#lister', count: listsShown(files).length };
133
- if (id === 'blokke') return { label: parts.blocks, href: '#blokke', count: blockParts.length };
134
- if (id === 'filer') return { label: parts.media, href: '#filer', count: mediaOf(files).length + filesOf(files).length };
135
- if (id === 'udseende') return { label: parts.appearance, href: '#udseende', count: null };
136
- 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 };
137
135
  if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
138
136
  if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
139
- 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 };
140
138
  const seoPart = seoParts.find((p) => p.id === id);
141
139
  if (seoPart) return { label: seoPart.label, href: `#seo/${id}`, count: null };
142
140
  // The screens under "Udseende" (Theme): the ones the editor has, then the site's own (appearance).
143
141
  const appearanceLabel = {
144
- temaer: parts.themes,
145
- ikonpakker: parts.iconPacks,
146
- bevaegelse: parts.motion,
147
- sidekolonne: parts.sideColumn,
148
- 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,
149
147
  logo: parts.brand,
150
- dele: parts.editorParts,
148
+ parts: parts.editorParts,
151
149
  }[id];
152
150
  const appearancePart = appearanceLabel ?? appearance.find((p) => p.id === id)?.label;
153
- if (appearancePart) return { label: appearancePart, href: `#udseende/${id}`, count: null };
151
+ if (appearancePart) return { label: appearancePart, href: `#appearance/${id}`, count: null };
154
152
  // The parts of a screen with parts ("Privatliv"): where each opens, or its screen's address with its id.
155
153
  for (const screen of partScreens) {
156
154
  const own = screen.parts.find((p) => p.id === id);
157
155
  if (own) return { label: own.label, href: own.href ?? `#${screen.id}/${id}`, count: null };
158
156
  }
159
157
  if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
160
- if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
161
- 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 };
162
160
  const screen = partScreens.find((s) => s.id === id);
163
161
  if (screen) return { label: screen.label, href: `#${id}`, count: null };
164
162
  if (id === 'seo') return { label: parts.seo, href: '#seo', count: null };
@@ -178,7 +176,7 @@ function PartRow({ id, label, href, count }: { id: string } & ReturnType<typeof
178
176
  }
179
177
 
180
178
  // The start screen: the site's name, the CMS version and when the site was last updated (built on Cloudflare, App.tsx), as StartStatus.tsx shows them, under it, and the FAB
181
- // menu for a new page or post (StartFab.tsx). The parts of the site are in the main drawer (MainMenu.tsx); the rest is left empty for now.
179
+ // menu for a new page or post (StartFab.tsx); each list has its own FAB for a new entry (ListFab.tsx). The parts of the site are in the main drawer (MainMenu.tsx); the rest is left empty for now.
182
180
  export function Start({ builtAt }: { builtAt: Date | null }) {
183
181
  return (
184
182
  <Screen
@@ -201,12 +199,12 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
201
199
  return (
202
200
  <Parts
203
201
  title={part.label}
204
- route={`virksomheden/${part.id}`}
202
+ route={`company/${part.id}`}
205
203
  parts={[...part.parts, ...appPartsIn(part, files)]}
206
- back={{ href: '#virksomheden', label: t.common.parts.company }}
204
+ back={{ href: '#company', label: t.common.parts.company }}
207
205
  />
208
206
  );
209
- return <Parts title={t.common.parts.company} route="virksomheden" parts={companyParts} />;
207
+ return <Parts title={t.common.parts.company} route="company" parts={companyParts} />;
210
208
  }
211
209
 
212
210
  // "Udseende": first "Temaer" (Themes), with the theme the site is built with at the end of its row (or, after a swap,
@@ -220,28 +218,28 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
220
218
  export function Theme({ files }: { files: RepoFile[] }) {
221
219
  const chosen = themes.find((x) => x.id === chosenTheme(files)) ?? theme;
222
220
  const parts: Part[] = [
223
- { 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 },
224
222
  {
225
- id: 'ikonpakker',
223
+ id: 'icon-packs',
226
224
  label: t.common.parts.iconPacks,
227
- icon: icons.ikonpakker,
225
+ icon: icons['icon-packs'],
228
226
  detail: iconPacks.find((x) => x.id === chosenPack(files))?.name,
229
227
  },
230
- { 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)] },
231
229
  {
232
- id: 'sidekolonne',
230
+ id: 'side-column',
233
231
  label: t.common.parts.sideColumn,
234
- icon: icons.sidekolonne,
232
+ icon: icons['side-column'],
235
233
  detail: sideChoicesOf(files).find((x) => x.type === sideSettingOf(files))?.label ?? t.library.sideNone,
236
234
  },
237
- { 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} /> },
238
236
  { id: 'logo', label: t.common.parts.brand, icon: icons.logo },
239
237
  { id: 'header', label: t.common.parts.header, icon: icons.header },
240
238
  { id: 'footer', label: t.common.parts.footer, icon: icons.footer },
241
239
  ...appearance,
242
- { id: 'dele', label: t.common.parts.editorParts, icon: icons.dele },
240
+ { id: 'parts', label: t.common.parts.editorParts, icon: icons.parts },
243
241
  ];
244
- 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')} />;
245
243
  }
246
244
 
247
245
  /** By "Farver": the site's primary, secondary and tertiary colours (accent, secondary, tertiary) in the mode the editor
@@ -254,8 +252,8 @@ function ThemeColors({ files }: { files: RepoFile[] }) {
254
252
  );
255
253
  }
256
254
 
257
- const backToAppearance = { href: '#udseende', label: t.common.parts.appearance };
258
- 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 };
259
257
 
260
258
  /** Under a theme's name: whether the site is built with it ("I brug") or is changing to it ("Skifter hertil"). */
261
259
  function themeStatus(id: string, files: RepoFile[]): string | undefined {
@@ -263,7 +261,7 @@ function themeStatus(id: string, files: RepoFile[]): string | undefined {
263
261
  if (id === chosenTheme(files)) return t.library.themeNext;
264
262
  }
265
263
 
266
- /** "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
267
265
  * does not have yet, as "Installeres…"), by name, each with its picture, its version and who made it, and whether it is
268
266
  * in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
269
267
  export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
@@ -283,7 +281,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
283
281
  return (
284
282
  <ListItem
285
283
  key={x.id}
286
- href={`#udseende/temaer/${x.id}`}
284
+ href={`#appearance/themes/${x.id}`}
287
285
  leading={<ThemePicture src={x.icon} />}
288
286
  headline={x.name}
289
287
  supporting={t.library.themeByline(x.version, x.author)}
@@ -298,7 +296,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
298
296
  );
299
297
  }
300
298
 
301
- /** "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
302
300
  * installed bring; iconPacks.ts), by name, each with who made it, its licence, the theme it came with and a few of its
303
301
  * icons as they are, the one chosen ticked (chosenPack), with "I brug" by the one the site and the editor are built with
304
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
@@ -367,7 +365,7 @@ export function motionOf(files: RepoFile[]): MotionScheme {
367
365
  return schemeInUse(scheme, theme.motion, themes.find((x) => x.id === STANDARD_THEME)?.motion);
368
366
  }
369
367
 
370
- /** "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
371
369
  * moves, the one in use ticked (motionOf) and "Temaets valg" by the theme's own. A tap on another asks first and then
372
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
373
371
  * once (applyMotion, App.tsx), the site after it is built. */
@@ -424,7 +422,7 @@ export function MotionSchemes({ env, files }: { env: Env; files: RepoFile[] }) {
424
422
  );
425
423
  }
426
424
 
427
- /** 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
428
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
429
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
430
428
  * commit, and the site is built with it; the pictures of the site in it (its screenshots/, packages/editor/scripts/theme-screenshots.mjs),
@@ -507,37 +505,37 @@ export function ThemePage({ of, env, files }: { of: ThemeInfo; env: Env; files:
507
505
  <ListItem key={text} icon={glyph} headline={text} />
508
506
  ))}
509
507
  </Group>
510
- <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} />
511
509
  <DeveloperRow name={of.author} />
512
510
  <ThemeStoreButtons of={of} env={env} files={files} />
513
511
  </Screen>
514
512
  );
515
513
  }
516
514
 
517
- /** "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
518
516
  * (Parts.tsx), the theme's UI components, the blocks (the standard ones and the apps') and each list of kinds
519
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). */
520
518
  export function PartsHome() {
521
519
  const parts: Part[] = [
522
- { id: 'editor', label: t.parts.screens.editor, icon: icons.dele },
523
- { id: 'ui', label: t.parts.screens.components, icon: icons.udseende },
524
- { id: 'blokke', label: t.parts.screens.blocks, icon: icons.blokke },
525
- ...kindLists().map((c) => ({ id: `liste/${c.id}`, label: c.label, icon: icons[c.id] ?? icons.lister })),
526
- { 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 },
527
525
  ];
528
- 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} />;
529
527
  }
530
528
 
531
- /** "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
532
530
  * the site's own parts (seoParts: "Interne links"), each on a screen of its own (CompanyForm.tsx). "SEO-vurdering"
533
- * (SeoScores.tsx, #seo/vurdering) is hidden: nothing links to it. */
531
+ * (SeoScores.tsx, #seo/rating) is hidden: nothing links to it. */
534
532
  export function Seo() {
535
- 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];
536
534
  return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
537
535
  }
538
536
 
539
537
  // A screen with parts of the site's own (partScreens): "Privatliv"; and with the settings of the apps installed with its
540
- // 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).
541
539
  export function PartsScreen({ screen, files }: { screen: PartScreen; files: RepoFile[] }) {
542
540
  const parts = [...screen.parts, ...appPartsIn(screen, files)];
543
541
  return (
@@ -553,11 +551,11 @@ export function PartsScreen({ screen, files }: { screen: PartScreen; files: Repo
553
551
  // and the funnel with the filters by group and by whether the block is on a page, each picked one as a chip.
554
552
  type BlockSort = 'group' | 'az' | 'za';
555
553
  const BLOCK_SORTS: BlockSort[] = ['group', 'az', 'za'];
556
- const BLOCK_SORT_KEY = 'editor-sort-blokke';
554
+ const BLOCK_SORT_KEY = 'editor-sort-blocks';
557
555
  type Use = '' | 'used' | 'unused';
558
556
 
559
557
  export function Blocks({ files }: { files: RepoFile[] }) {
560
- const [query, setQuery] = useKept('blokke-soeg', '');
558
+ const [query, setQuery] = useKept('blocks-search', '');
561
559
  const [sort, setSort] = useState<BlockSort>(() => {
562
560
  try {
563
561
  const saved = localStorage.getItem(BLOCK_SORT_KEY);
@@ -565,8 +563,8 @@ export function Blocks({ files }: { files: RepoFile[] }) {
565
563
  } catch {}
566
564
  return 'group';
567
565
  });
568
- const [group, setGroup] = useKept('blokke-gruppe', '');
569
- const [use, setUse] = useKept<Use>('blokke-brug', '');
566
+ const [group, setGroup] = useKept('blocks-group', '');
567
+ const [use, setUse] = useKept<Use>('blocks-use', '');
570
568
  // The blocks on a page (any copy, also a hidden one), by their type (a part's id).
571
569
  const used = useMemo(() => new Set(blockParts.filter((p) => pagesWith(p.id, files).length).map((p) => p.id)), [files]);
572
570
  const bothUses = blockParts.some((p) => used.has(p.id)) && blockParts.some((p) => !used.has(p.id));
@@ -650,9 +648,9 @@ export function Blocks({ files }: { files: RepoFile[] }) {
650
648
  {!inGroups.length ? (
651
649
  <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
652
650
  ) : sort === 'group' ? (
653
- 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} />)
654
652
  ) : (
655
- 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} />)
656
654
  )}
657
655
  </Screen>
658
656
  );
@@ -975,7 +973,7 @@ type View = 'liste' | 'menu';
975
973
  const seoKey = (c: Collection) => `editor-seo-${c.id}`;
976
974
  function savedSeo(c: Collection): boolean {
977
975
  try {
978
- return localStorage.getItem(seoKey(c)) === 'vis';
976
+ return localStorage.getItem(seoKey(c)) === 'show';
979
977
  } catch {
980
978
  return false;
981
979
  }
@@ -1006,16 +1004,16 @@ const saved = new Map<string, { oid: string; date: Date }>();
1006
1004
 
1007
1005
  // The ways a list can be sorted; by time only where the entries have one: blog posts by their date,
1008
1006
  // pages by when they were last saved; by their kind where they have kinds (Promovering).
1009
- type Sort = 'nyeste' | 'aeldste' | 'az' | 'za' | 'type';
1007
+ type Sort = 'newest' | 'oldest' | 'az' | 'za' | 'type';
1010
1008
  const sortLabel = (c: Collection, s: Sort) => {
1011
1009
  const { sorts } = t.library;
1012
- 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];
1013
1011
  };
1014
1012
 
1015
1013
  const byTitle = (a: Item, b: Item) => byText(a.title, b.title);
1016
1014
  const compare: Record<Sort, (a: Item, b: Item) => number> = {
1017
- nyeste: (a, b) => (b.date ?? '').localeCompare(a.date ?? '') || byTitle(a, b),
1018
- 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),
1019
1017
  az: byTitle,
1020
1018
  za: (a, b) => byTitle(b, a),
1021
1019
  type: (a, b) => byText(a.kind ?? '', b.kind ?? '') || byTitle(a, b),
@@ -1041,7 +1039,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1041
1039
  // The tabs the apps installed add to it, and the one open (none: the list's own).
1042
1040
  const appTabs = tabsOf(collection.id, apps, installedApps(files));
1043
1041
  const appTab = appTabs.find((a) => a.app.id === tab);
1044
- const [query, setQuery] = useKept(`${collection.id}-soeg`, '');
1042
+ const [query, setQuery] = useKept(`${collection.id}-search`, '');
1045
1043
  // The move that runs (row and target row, null: none); only its arrow shows busy, all arrows stay disabled meanwhile.
1046
1044
  const [moving, setMoving] = useState<{ from: number; to: number } | null>(null);
1047
1045
  const busy = moving !== null;
@@ -1060,7 +1058,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1060
1058
  collection.kind === 'files' || (collection.kind === 'folder' && collection.orderable) || items.length < 2
1061
1059
  ? []
1062
1060
  : collection.id === 'blog' || pages
1063
- ? ['nyeste', 'aeldste', 'az', 'za']
1061
+ ? ['newest', 'oldest', 'az', 'za']
1064
1062
  : collection.kind === 'folder' && collection.kinds
1065
1063
  ? ['az', 'za', 'type']
1066
1064
  : ['az', 'za'];
@@ -1101,7 +1099,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1101
1099
  // A list of what is shown on pages: only those on one page or the posts of one tag (and those on every
1102
1100
  // page), picked among the pages and tags the entries are on, by their names (relationsOf), the tags
1103
1101
  // under a heading of their own (`tag:<slug>`); all again when that one is no longer among them.
1104
- const [onPage, setOnPage] = useKept(`${collection.id}-side`, '');
1102
+ const [onPage, setOnPage] = useKept(`${collection.id}-page`, '');
1105
1103
  const pageNames = useMemo(() => {
1106
1104
  if (!items.some((i) => i.shownOn)) return [];
1107
1105
  const { pages: all, fixed, categories } = relationsOf(files);
@@ -1249,7 +1247,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1249
1247
  function pickSeo(next: boolean) {
1250
1248
  setSeo(next);
1251
1249
  try {
1252
- localStorage.setItem(seoKey(collection), next ? 'vis' : 'skjul');
1250
+ localStorage.setItem(seoKey(collection), next ? 'show' : 'hide');
1253
1251
  } catch {}
1254
1252
  }
1255
1253
 
@@ -1336,7 +1334,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1336
1334
  />
1337
1335
  );
1338
1336
  };
1339
- const privacyScreen = partScreens.find((s) => s.id === 'privatliv');
1337
+ const privacyScreen = partScreens.find((s) => s.id === 'privacy');
1340
1338
  return (
1341
1339
  <Screen
1342
1340
  title={collection.label}
@@ -1345,9 +1343,9 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1345
1343
  collection.id === config.lists.cookies && privacyScreen
1346
1344
  ? { href: `#${privacyScreen.id}`, label: privacyScreen.label }
1347
1345
  : lists.includes(collection.id)
1348
- ? { href: '#lister', label: t.common.parts.lists }
1346
+ ? { href: '#lists', label: t.common.parts.lists }
1349
1347
  : header.includes(collection.id)
1350
- ? { href: '#udseende/header', label: t.common.parts.header }
1348
+ ? { href: '#appearance/header', label: t.common.parts.header }
1351
1349
  : { href: '#', label: siteName }
1352
1350
  }
1353
1351
  tabs={
@@ -1359,7 +1357,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1359
1357
  { value: '', label: collection.label, icon: icons[collection.id as keyof typeof icons] ?? ListIcon },
1360
1358
  ...appTabs.map((a) => ({ value: a.app.id, label: siteText(a.tab.label), icon: a.tab.icon })),
1361
1359
  ],
1362
- 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}`),
1363
1361
  }
1364
1362
  : undefined
1365
1363
  }
@@ -1371,16 +1369,9 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1371
1369
  icon={ClockCounterClockwise}
1372
1370
  label={t.history.deletedOf(collection.label)}
1373
1371
 
1374
- onClick={() => (location.hash = `slettet/${collection.id}`)}
1372
+ onClick={() => (location.hash = `deleted/${collection.id}`)}
1375
1373
  />
1376
1374
  )}
1377
- {/* Of more than one kind (a pop-up: Standard or an app's, e.g. Mailchimp): a sheet to pick it. */}
1378
- {collection.create &&
1379
- (kinds.length > 1 ? (
1380
- <NewChoice title={collection.create} options={kinds} onChange={(kind) => (location.hash = `#ny/${collection.id}/${kind}`)} />
1381
- ) : (
1382
- <IconLink href={`#ny/${collection.id}`} label={collection.create!} icon={Plus} />
1383
- ))}
1384
1375
  </>
1385
1376
  }
1386
1377
  >
@@ -1479,6 +1470,8 @@ export function List({ collection, tab, files, env }: { collection: Collection;
1479
1470
  ) : (
1480
1471
  <p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
1481
1472
  )}
1473
+ {/* A new entry: the list's FAB, or a FAB menu of its kinds when it has several (a pop-up: Standard or an app's, e.g. Mailchimp). */}
1474
+ <ListFab collection={collection} kinds={kinds} />
1482
1475
  </Screen>
1483
1476
  );
1484
1477
  }
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),
@@ -0,0 +1,13 @@
1
+ // The create FAB of a list (List in Library.tsx): its own create action, or a menu of its kinds when it has several.
2
+ // Exposes ListFab. Uses FabMenu (ui.tsx), listFab (fabScreens.ts) and the icons of site.ts.
3
+ import { Plus } from '@phosphor-icons/react';
4
+ import { listFab } from './fabScreens';
5
+ import { icons } from './site';
6
+ import { FabMenu } from './ui';
7
+
8
+ export function ListFab({ collection, kinds }: { collection: { id: string; create?: string }; kinds: { value: string; label: string }[] }) {
9
+ // One action is the FAB's (the +); each of several kinds has its list's icon.
10
+ const actions = listFab(collection, kinds);
11
+ const icon = actions.length > 1 ? (icons[collection.id] ?? Plus) : Plus;
12
+ return <FabMenu icon={Plus} label={collection.create ?? ''} items={actions.map((a) => ({ ...a, icon }))} />;
13
+ }
@@ -1,4 +1,4 @@
1
- // The main drawer and its menu button on #udseende/dele/editor (Parts.tsx): the modal drawer in its two levels (sections, and
1
+ // The main drawer and its menu button on #appearance/parts/editor (Parts.tsx): the modal drawer in its two levels (sections, and
2
2
  // a section's list with the back row and a fold-out), opened from a button, so it can be tried at any width, and the menu
3
3
  // button open and closed. The drawer's header is a sample user (AccountRow). Exposes MainDrawerSample. Uses MainDrawer and
4
4
  // its parts from ui.tsx.
@@ -53,8 +53,8 @@ export function MainDrawerSample() {
53
53
  {section === null ? (
54
54
  <MainDrawerLevel>
55
55
  <MainDrawerItem href="" icon={icons.home} label={parts.company} />
56
- <MainDrawerSectionItem icon={icons.globalt} label={parts.header} selected onClick={() => setSection('globalt')} />
57
- <MainDrawerSectionItem icon={icons.udseende} label={parts.appearance} onClick={() => setSection('udseende')} />
56
+ <MainDrawerSectionItem icon={icons.global} label={parts.header} selected onClick={() => setSection('global')} />
57
+ <MainDrawerSectionItem icon={icons.appearance} label={parts.appearance} onClick={() => setSection('appearance')} />
58
58
  </MainDrawerLevel>
59
59
  ) : (
60
60
  <MainDrawerLevel focus="list">
package/src/MainMenu.tsx CHANGED
@@ -86,7 +86,7 @@ export function MainMenu({ route, files, user }: { route: string; files: RepoFil
86
86
  onEscape={onEscape}
87
87
  header={
88
88
  shown === null ? (
89
- user && <AccountRow user={user} supporting={config.domain} href="#konto" />
89
+ user && <AccountRow user={user} supporting={config.domain} href="#account" />
90
90
  ) : (
91
91
  <MainDrawerBack label={t.library.back} name={t.common.backTo(t.library.mainMenu)} onClick={() => show(null)} />
92
92
  )