@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
@@ -7,11 +7,12 @@
7
7
  // (the standard blocks, each app's under its name), the one picked as a chip. Also the sheet of a section's
8
8
  // settings (its colour, SectionSettings.tsx), which goes the same way as a block's fields: Annuller, Gem, the question when closed changed.
9
9
  // The fields and the section settings are in the theme's side sheet (sheetPlacement.mjs): a standard one beside the page from
10
- // medium (not modal: the page stays usable, .editor-side-sheet in editor.css), a modal one over the page below it
10
+ // expanded (not modal: the page stays usable, .editor-side-sheet in editor.css), a modal one over the page below it
11
11
  // (fields only; the section settings are a sheet from the bottom there). Both end with Gem (Indsæt) and Annuller.
12
12
  // Their open, close, focus and Escape are the theme's (sideSheet() and standardSideSheet(), sideSheetControl.ts); leave() is their beforeClose.
13
- // Another block tapped while one is open closes the open one first (the question when changed). Exposes useBlockDialog, IMAGE.
14
- import { useEffect, useId, useRef, useState, type ReactElement } from 'react';
13
+ // Another block tapped while one is open closes the open one first (the question when changed). In the standard side sheet a
14
+ // card already in the text also has "Remove" after Cancel in the actions (the answer 'remove'). Exposes useBlockDialog, IMAGE.
15
+ import { useEffect, useId, useRef, useState, type CSSProperties, type ReactElement } from 'react';
15
16
  import { ArrowsDownUp, Image } from '@phosphor-icons/react';
16
17
  import {
17
18
  Button,
@@ -46,6 +47,7 @@ import { snackbar } from '../lib/snackbar';
46
47
  import { closeChanged } from './ask';
47
48
  import SectionSettings from './SectionSettings';
48
49
  import { sheetLook } from './sheetPlacement.mjs';
50
+ import { setPanelOpen } from './sidePanel';
49
51
  import { SiteSideSheet, siteSideSheet, siteSideSheetDialogClass, siteStandardSideSheet } from './themeUi';
50
52
 
51
53
  export type BlockAsk = {
@@ -58,13 +60,17 @@ export type BlockAsk = {
58
60
  * than once, unless the template says how many (have: the types in the text now, one for each, IMAGE for a picture).
59
61
  * image: a picture is offered first, and chosen as the type IMAGE. */
60
62
  choose: (offer?: { image?: boolean; have?: string[] }) => Promise<{ type: string; props: BlockProps } | null>;
61
- /** New props for a block in the text; null when cancelled. isNew: one about to be put in ("Indsæt"). */
62
- edit: (type: string, props: BlockProps, isNew?: boolean) => Promise<BlockProps | null>;
63
+ /** New props for a block in the text; null when cancelled. isNew: one about to be put in ("Indsæt"). removable: the
64
+ * card may be removed, so the side panel has "Remove"; 'remove' when it is tapped (never when removable is not true). */
65
+ edit: (type: string, props: BlockProps, isNew?: boolean, removable?: boolean) => Promise<BlockProps | 'remove' | null>;
63
66
  /** The settings of the section the cursor is in (now its colour; null: none); the new ones, or null when cancelled. */
64
67
  section: (color: string | null) => Promise<{ color: string | null } | null>;
65
68
  };
66
69
 
67
- type Open = { step: 'choose'; image?: boolean; have?: string[] } | { step: 'fields'; type: string; isNew: boolean } | { step: 'section' };
70
+ type Open =
71
+ | { step: 'choose'; image?: boolean; have?: string[] }
72
+ | { step: 'fields'; type: string; isNew: boolean; removable?: boolean }
73
+ | { step: 'section' };
68
74
 
69
75
  /** The type a section's settings are answered with (not a block's). */
70
76
  const SECTION = 'section';
@@ -99,7 +105,7 @@ const picture: SiteBlock = {
99
105
  },
100
106
  ],
101
107
  };
102
- type Answer = { type: string; props: BlockProps } | null;
108
+ type Answer = { type: string; props: BlockProps } | 'remove' | null;
103
109
 
104
110
  // Text as searched: lower case, without accents (as in PagePicker.tsx).
105
111
  const plain = (s: string) =>
@@ -110,7 +116,7 @@ const plain = (s: string) =>
110
116
 
111
117
  type InsertSort = 'az' | 'za' | 'group';
112
118
  const INSERT_SORTS: InsertSort[] = ['az', 'za', 'group'];
113
- const INSERT_SORT_KEY = 'editor-sort-indsaet';
119
+ const INSERT_SORT_KEY = 'editor-sort-insert';
114
120
 
115
121
  // A block's group: the app's name for an app's block, otherwise the standard blocks (also the picture and HTML).
116
122
  const groupOf = (b: SiteBlock) => (b.app ? (apps.find((a) => a.id === b.app)?.label ?? b.app) : t.blockSettings.standard);
@@ -124,13 +130,13 @@ export function useBlockDialog(
124
130
  ): [BlockAsk, ReactElement] {
125
131
  const blockOf = (type: string) => (type === IMAGE ? picture : type === HTML ? htmlBlock : list.find((b) => b.type === type));
126
132
  const dialog = useRef<HTMLDialogElement>(null);
127
- // The modal side sheet's <dialog> (below medium) and its controls; the standard one's box (from medium).
133
+ // The modal side sheet's <dialog> (below expanded) and its controls; the standard one's box (from expanded).
128
134
  const sideDialog = useRef<HTMLDialogElement>(null);
129
135
  const sideControls = useRef<ReturnType<typeof siteSideSheet> | null>(null);
130
136
  const standard = useRef<HTMLDivElement>(null);
131
137
  const standardControls = useRef<ReturnType<typeof siteStandardSideSheet> | null>(null);
132
138
  const headingId = useId();
133
- const medium = useAtLeast('medium');
139
+ const expanded = useAtLeast('expanded');
134
140
  const answer = useRef<(a: Answer) => void>(() => {});
135
141
  const [open, setOpen] = useState<Open | null>(null);
136
142
  // What is open now (set at once, where `open` follows a render later), the look it has, when the last one has closed
@@ -186,7 +192,7 @@ export function useBlockDialog(
186
192
  };
187
193
 
188
194
  // Which dialog is open follows the look of the step: the sheet from the bottom, or the modal side sheet (below).
189
- const look: string = open ? sheetLook(open.step, medium) : 'sheet';
195
+ const look: string = open ? sheetLook(open.step, expanded) : 'sheet';
190
196
  lookNow.current = look;
191
197
  useEffect(() => {
192
198
  const sheet = dialog.current;
@@ -202,7 +208,7 @@ export function useBlockDialog(
202
208
  return false;
203
209
  };
204
210
 
205
- // The modal side sheet (below medium).
211
+ // The modal side sheet (below expanded).
206
212
  useEffect(() => {
207
213
  const d = sideDialog.current;
208
214
  if (look !== 'modal-side-sheet' || !d) return;
@@ -214,11 +220,12 @@ export function useBlockDialog(
214
220
  };
215
221
  }, [look]);
216
222
 
217
- // The standard side sheet (from medium): the page makes room for it (editor.css: [data-side-panel]), the focus moves
223
+ // The standard side sheet (from expanded): the page makes room for it (editor.css: [data-side-panel]), the focus moves
218
224
  // into it and back to what had it before when it closes.
219
225
  useEffect(() => {
220
226
  if (look !== 'side-sheet') return;
221
227
  document.documentElement.setAttribute('data-side-panel', '');
228
+ setPanelOpen(true);
222
229
  const box = standard.current?.querySelector<HTMLElement>('[data-side-sheet-container]');
223
230
  if (box) {
224
231
  standardControls.current = siteStandardSideSheet(box, { beforeClose });
@@ -227,6 +234,7 @@ export function useBlockDialog(
227
234
  return () => {
228
235
  standardControls.current = null;
229
236
  document.documentElement.removeAttribute('data-side-panel');
237
+ setPanelOpen(false);
230
238
  };
231
239
  }, [look]);
232
240
 
@@ -254,16 +262,23 @@ export function useBlockDialog(
254
262
  list,
255
263
  template,
256
264
  data,
257
- choose: (offer) => ask({ step: 'choose', image: offer?.image, have: offer?.have }),
265
+ choose: async (offer) => {
266
+ const answered = await ask({ step: 'choose', image: offer?.image, have: offer?.have });
267
+ return answered === 'remove' ? null : answered;
268
+ },
258
269
  section: async (color) => {
259
270
  const answered = await ask({ step: 'section' }, { color: color ?? '' });
260
- return answered && { color: answered.props.color ? String(answered.props.color) : null };
271
+ if (!answered || answered === 'remove') return null;
272
+ return { color: answered.props.color ? String(answered.props.color) : null };
261
273
  },
262
- edit: async (type, props, isNew = false) => {
274
+ edit: async (type, props, isNew = false, removable = false) => {
263
275
  const fields = blockOf(type)?.fields ?? [];
264
276
  // A block on the page with what a field it has no value for means; a new one with the defaults.
265
- const a = await ask({ step: 'fields', type, isNew }, isNew ? { ...newValue(fields), ...props } : savedValue(fields, props));
266
- return a && a.props;
277
+ const a = await ask(
278
+ { step: 'fields', type, isNew, removable },
279
+ isNew ? { ...newValue(fields), ...props } : savedValue(fields, props),
280
+ );
281
+ return a === 'remove' ? a : a && a.props;
267
282
  },
268
283
  };
269
284
 
@@ -378,6 +393,12 @@ export function useBlockDialog(
378
393
  <Button kind="outlined" onClick={() => void leave()}>
379
394
  {t.common.cancel}
380
395
  </Button>
396
+ {open.step === 'fields' && open.removable && !open.isNew && look === 'side-sheet' && (
397
+ // After Save and Cancel in the start-aligned row, in the error colour (the theme's text button with its colour variable).
398
+ <Button kind="text" style={{ '--button-text-text': 'var(--error)' } as CSSProperties} onClick={() => done('remove')}>
399
+ {t.common.remove}
400
+ </Button>
401
+ )}
381
402
  </>
382
403
  }
383
404
  >
@@ -1,5 +1,5 @@
1
- // "Dele" › "UI-komponenter" (#udseende/dele/ui), "Dele" › "Blokke" (#udseende/dele/blokke) and "Dele" › a list of kinds
2
- // ("Promovering", #udseende/dele/liste/<id>): how the site looks, every part in every version, to see and agree on it.
1
+ // "Dele" › "UI-komponenter" (#appearance/parts/ui), "Dele" › "Blokke" (#appearance/parts/blocks) and "Dele" › a list of kinds
2
+ // ("Promovering", #appearance/parts/list/<id>): how the site looks, every part in every version, to see and agree on it.
3
3
  // "UI-komponenter" also has the active theme's coverage (summary, mark on each part, filter by style) and the site frame's parts.
4
4
  // "UI-komponenter" has the theme's UI components (its buttons and icon buttons, text fields and checkboxes …,
5
5
  // packages/editor/theme/ui/, through @site/editor) in every kind, size and state; "Blokke" every block, the standard
@@ -52,7 +52,7 @@ import { t } from './i18n';
52
52
  import { Svg } from './icon';
53
53
 
54
54
  const { screens } = t.parts;
55
- const back = { href: '#udseende/dele', label: t.common.parts.editorParts };
55
+ const back = { href: '#appearance/parts', label: t.common.parts.editorParts };
56
56
 
57
57
  // The fields filled in with the plain picture where they have none (a card's, a list's items').
58
58
  function pictured(fields: Field[], props: BlockProps): BlockProps {
@@ -520,7 +520,7 @@ function CardParts() {
520
520
  );
521
521
  }
522
522
 
523
- /** "Dele" › "UI-komponenter" (#udseende/dele/ui): the theme's UI components in every kind, size and state. */
523
+ /** "Dele" › "UI-komponenter" (#appearance/parts/ui): the theme's UI components in every kind, size and state. */
524
524
  export function ComponentParts() {
525
525
  const groups = [
526
526
  { id: 'ui', title: screens.ui },
@@ -587,7 +587,7 @@ export function ComponentParts() {
587
587
  );
588
588
  }
589
589
 
590
- /** "Dele" › "Blokke" (#udseende/dele/blokke): every block, the standard ones and then the apps', each a group of its
590
+ /** "Dele" › "Blokke" (#appearance/parts/blocks): every block, the standard ones and then the apps', each a group of its
591
591
  * own, in all its versions. */
592
592
  export function BlocksParts() {
593
593
  const data = useMemo(exampleData, []);
@@ -616,7 +616,7 @@ export function BlocksParts() {
616
616
  /** The lists of kinds ("Promovering"), each a screen of "Dele". */
617
617
  export const kindLists = () => collections.filter((c): c is FolderCollection => c.kind === 'folder' && !!c.kinds && !!c.fields?.length);
618
618
 
619
- /** "Dele" › a list of kinds ("Promovering", #udseende/dele/liste/<id>): each of its kinds as the site draws it, its own
619
+ /** "Dele" › a list of kinds ("Promovering", #appearance/parts/list/<id>): each of its kinds as the site draws it, its own
620
620
  * (FolderCollection.draws: the pop-up, the bars) and the apps' (AppKind: Mailchimp's signup, once for each of its
621
621
  * states), each a group of its own, as a new one of the kind with the made-up texts (kindExample). */
622
622
  export function KindParts({ id }: { id: string }) {
@@ -1,4 +1,4 @@
1
- // "Blokke" › "Blokindstillinger" (#blokvalg): the blocks that can be put in the text of a page, in
1
+ // "Blokke" › "Blokindstillinger" (#block-settings): the blocks that can be put in the text of a page, in
2
2
  // groups, the editor's standard blocks (packages/editor/src/standard/) and those of each app installed under "Apps",
3
3
  // under the app's name (e.g. "Mailchimp",
4
4
  // src/apps/, AppList.tsx; an app not installed has none here), each with how many pages it is on at its right
@@ -243,7 +243,7 @@ export function KindSwitch({ app, list, env, files }: { app: SiteApp; list: stri
243
243
  type Sort = 'group' | 'az' | 'za';
244
244
  const SORTS: Sort[] = ['group', 'az', 'za'];
245
245
  // The order picked last time, in this browser.
246
- const SORT_KEY = 'editor-sort-blokvalg';
246
+ const SORT_KEY = 'editor-sort-block-settings';
247
247
  type OnOff = '' | 'on' | 'off';
248
248
  type Use = '' | 'used' | 'unused';
249
249
 
package/src/BrandForm.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Logo og ikon" (#udseende/logo): a new logo (the mark by the site's name) and a new icon (the browser
1
+ // "Udseende" › "Logo og ikon" (#appearance/logo): a new logo (the mark by the site's name) and a new icon (the browser
2
2
  // tab, Google and the editor's app icon), made from a picture the owner picks (brand.ts), in the site's colours
3
3
  // ("Farver"). Both are saved together in one commit with "Gem ændringer".
4
4
  import { useEffect, useState } from 'react';
@@ -120,7 +120,7 @@ export default function BrandForm({ env }: { env: Env }) {
120
120
  return (
121
121
  <FormFrame
122
122
  title={t.common.parts.brand}
123
- back={{ href: '#udseende', label: t.common.parts.appearance }}
123
+ back={{ href: '#appearance', label: t.common.parts.appearance }}
124
124
  dirty={dirty}
125
125
  saving={saving || drawing}
126
126
  onSave={save}
package/src/Colors.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Farver" (#udseende/farver): the site's colours, for light and dark mode, in the site's file of
1
+ // "Udseende" › "Farver" (#appearance/colors): the site's colours, for light and dark mode, in the site's file of
2
2
  // colours (config.colors, packages/editor/lib/colors.ts). The first tab, "Generator", where the screen starts, or one
3
3
  // mode at a time, picked with the tabs after it, and the editor is shown in the mode while the screen is open
4
4
  // (theme.ts holdTheme). Each mode has a little page drawn in its colours, the pairs that are too hard to
@@ -15,7 +15,7 @@
15
15
  // farver", src/apps/content-colors/: pageColors in packages/editor/site/pageColors.ts). While an app installed changes the pages'
16
16
  // colours (hooks/page-colors.ts), each mode shows the pages' colours instead (PageColors), as the site was last built
17
17
  // (/editor/page-colors.json, packages/editor/scripts/page-colors.mjs): first a notice that the apps give them, with a button to the
18
- // site's own, which are only the fallback ("Reservefarver", #udseende/farver/reservefarver, a page of the same form, so
18
+ // site's own, which are only the fallback ("Reservefarver", #appearance/colors/fallback-colors, a page of the same form, so
19
19
  // what is unsaved stays), then the apps, each to its settings, the pages with colours from a picture (the picture, the
20
20
  // page and its colours in the mode open), and those with the site's own. Saved with "Gem ændringer" in one commit; the
21
21
  // editor shows the new colours at once (theme.ts applyColors), the site when it is built again.
@@ -112,7 +112,7 @@ const CHANGE = t.app.changes.edited(t.app.changes.colors);
112
112
  type Tab = Mode | 'generator';
113
113
 
114
114
  /** The page with the site's own colours, while an app changes the pages' colours: the fallback. */
115
- export const FALLBACK_ROUTE = 'udseende/farver/reservefarver';
115
+ export const FALLBACK_ROUTE = 'appearance/colors/fallback-colors';
116
116
 
117
117
  /** The apps installed that change the pages' colours (hooks/page-colors.ts). */
118
118
  const colorApps = (files: RepoFile[]) => {
@@ -139,7 +139,7 @@ export default function Colors({ env, files, fallback }: { env: Env; files: Repo
139
139
  const changing = useMemo(() => colorApps(files), [files]);
140
140
  useEffect(() => () => holdTheme(null), []);
141
141
  // While an app changes the pages' colours, the modes show the pages', and the site's own are on a page of the form.
142
- useFormPages(changing.length ? ['udseende/farver', FALLBACK_ROUTE] : []);
142
+ useFormPages(changing.length ? ['appearance/colors', FALLBACK_ROUTE] : []);
143
143
  const ownShown = fallback || !changing.length;
144
144
  const appNames = new Intl.ListFormat(locale).format(changing.map((a) => a.label));
145
145
  const file = files.find((f) => f.path === config.colors);
@@ -207,8 +207,8 @@ export default function Colors({ env, files, fallback }: { env: Env; files: Repo
207
207
  title={changing.length && fallback ? t.colors.pages.fallback : t.common.parts.colors}
208
208
  back={
209
209
  changing.length && fallback
210
- ? { href: '#udseende/farver', label: t.common.parts.colors, guard: (go) => go() }
211
- : { href: '#udseende', label: t.common.parts.appearance }
210
+ ? { href: '#appearance/colors', label: t.common.parts.colors, guard: (go) => go() }
211
+ : { href: '#appearance', label: t.common.parts.appearance }
212
212
  }
213
213
  dirty={dirty}
214
214
  saving={saving}
@@ -113,7 +113,7 @@ export default function CompanyForm({ part, env, files, at: route, page }: Props
113
113
  ) : (
114
114
  <FormFrame
115
115
  title={part.label}
116
- back={part.back ?? { href: '#virksomheden', label: t.common.parts.company }}
116
+ back={part.back ?? { href: '#company', label: t.common.parts.company }}
117
117
  dirty={dirty}
118
118
  saving={saving}
119
119
  onSave={save}
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);
@@ -0,0 +1,8 @@
1
+ // A screen's one create action as a FAB (Files, Redirects, a single-file list form): a button, busy and
2
+ // disabled as the screen says. Exposes CreateFab. Uses FabMenu (ui.tsx).
3
+ import { Plus } from '@phosphor-icons/react';
4
+ import { FabMenu } from './ui';
5
+
6
+ export function CreateFab({ label, onClick, busy, disabled }: { label: string; onClick: () => void; busy?: boolean; disabled?: boolean }) {
7
+ return <FabMenu icon={Plus} label={label} items={[{ icon: Plus, label, onClick, busy, disabled }]} />;
8
+ }
package/src/DataForm.tsx CHANGED
@@ -4,7 +4,6 @@
4
4
  // is opened as changes that are not saved. Exposes DataForm (default). Its values from the file and back: dataFileValues.ts;
5
5
  // a menu's fields: DataFileFields.tsx.
6
6
  import { useMemo, useState } from 'react';
7
- import { Plus } from '@phosphor-icons/react';
8
7
  import { entryName, slugify, slugOf, type DataFile, type FilesCollection, type FolderCollection } from './schema';
9
8
  import { config, header, keptBy, sharing, siteName, usesOf } from '@site/editor';
10
9
  import { apps, asWritten, collections } from './site';
@@ -12,6 +11,7 @@ import { kindDraw } from './apps';
12
11
  import { ConflictError, type RepoFile } from './github';
13
12
  import { clean, newItem, newValue, problemAt } from './check';
14
13
  import { FieldInput, FieldList, Fields, useWrongField, WrongField, type FieldContext } from './fields';
14
+ import { CreateFab } from './CreateFab';
15
15
  import { DataFileFields } from './DataFileFields';
16
16
  import { dataOf, fieldsOf, listOnly, valuesOf } from './dataFileValues';
17
17
  import SectionsField from './SectionsField';
@@ -21,7 +21,7 @@ import { splitPicture } from '../lib/crop.mjs';
21
21
  import { useMediaPicker } from './Media';
22
22
  import FormFrame, { savedStatus, useSavedAt } from './FormFrame';
23
23
  import { savedOn, VersionNotice, VersionsButton, WithVersions, type ShownVersion } from './Versions';
24
- import { Button, CheckList, FoldSection, FormColumns, Group, IconButton, Preview, FormHeading, SharePreview, Notice, ListItem } from './ui';
24
+ import { Button, CheckList, FoldSection, FormColumns, Group, Preview, FormHeading, SharePreview, Notice, ListItem } from './ui';
25
25
  import { SearchPreview } from './SearchPreview';
26
26
  import { searchedAs } from './googleResult.mjs';
27
27
  import { plain, SEO_FIELDS, seoRating, SHARE_FIELDS, sharedAs } from './seo';
@@ -34,7 +34,7 @@ type Props = {
34
34
  dataFile?: DataFile;
35
35
  file: RepoFile;
36
36
  env: Env;
37
- /** A new entry of this kind (FolderCollection.kinds), picked under "+". */
37
+ /** A new entry of this kind (FolderCollection.kinds), picked in the list's create FAB menu. */
38
38
  kind?: string;
39
39
  };
40
40
 
@@ -294,7 +294,7 @@ function Form({
294
294
  label: v.label,
295
295
  node: draw({ ...values, ...v.props }, { lists: env.lists, testimonials: env.testimonials, relations: env.relations, site: env.site }),
296
296
  }));
297
- // A file that is a list (the menus) is added to with "+" in the top bar, like the other lists; its own fields
297
+ // A file that is a list (the menus) is added to with the FAB, like the other lists; its own fields
298
298
  // ("Aktiveret") stand above it.
299
299
  const root = dataFile?.root;
300
300
  // Where the entry is used on the site (a tag: its posts, testimonials, promotions and blocks), each opening it.
@@ -309,28 +309,16 @@ 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}
321
- actions={
322
- <>
323
- {!isNew && <VersionsButton env={env} path={base.path} onPick={onVersion} />}
324
- {root && (
325
- <IconButton
326
- icon={Plus}
327
- label={t.forms.addOne(root.singular!)}
328
-
329
- onClick={() => setItems([...items, newItem(root)])}
330
- />
331
- )}
332
- </>
333
- }
321
+ actions={!isNew && <VersionsButton env={env} path={base.path} onPick={onVersion} />}
334
322
  dirty={dirty}
335
323
  saving={saving || deleting}
336
324
  deleting={deleting}
@@ -424,6 +412,8 @@ function Form({
424
412
  ))}
425
413
  </Group>
426
414
  )}
415
+ {/* A single-file list: a new item in it. */}
416
+ {root && <CreateFab label={t.forms.addOne(root.singular!)} onClick={() => setItems([...items, newItem(root)])} />}
427
417
  </FormFrame>
428
418
  {picker}
429
419
  </WrongField.Provider>
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}
@@ -641,7 +641,7 @@ function Form({
641
641
  <>
642
642
  <FoldSection {...sectionProps('content')}>
643
643
  {collection.fields.filter((f) => TOP.includes(f.name) && !STATUS.includes(f.name)).map(field)}
644
- <div ref={textBox} className="scroll-mt-[calc(5rem+env(safe-area-inset-top))]">
644
+ <div ref={textBox} className="scroll-mt-[calc(1rem+env(safe-area-inset-top))]">
645
645
  <span className="mb-1 block font-medium">{t.forms.text}</span>
646
646
  <MarkdownEditor
647
647
  ref={text}
@@ -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);
package/src/FormFrame.tsx CHANGED
@@ -3,7 +3,8 @@
3
3
  // of the screen with "Gem" (across the bar on a phone, at its end from medium), which is only there when something
4
4
  // has been changed. Messages are shown by the app (Env.notify); the first "Gem" asks whether the editor may also send notifications.
5
5
  // Leaving with changes that are not saved asks first, to save them or not (in the editor's dialog;
6
- // closing or reloading the page asks in the browser's own). "Forhåndsvis" beside "Gem" and in the top bar shows the page with
6
+ // closing or reloading the page asks in the browser's own). "Forhåndsvis" beside "Gem" and, as a page action (Screen.tsx `pageActions`:
7
+ // the end of the title row from expanded, the top bar below it), shows the page with
7
8
  // what is in the form, before it is saved (pagePreview.tsx).
8
9
  import { useEffect, useRef, useState, type ReactNode } from 'react';
9
10
  import { ArrowSquareOut, Binoculars, Trash } from '@phosphor-icons/react';
@@ -177,9 +178,9 @@ export default function FormFrame<T extends string>({
177
178
  drawer={drawer}
178
179
  tabs={tabs}
179
180
  back={{ guard: leaveTo, ...back }}
180
- actions={
181
+ actions={actions}
182
+ pageActions={
181
183
  <>
182
- {actions}
183
184
  {onPreview && <IconButton icon={Binoculars} label={t.preview.button} aria-haspopup="dialog" onClick={onPreview} />}
184
185
  {view && <IconLink href={view} target="_blank" rel="noopener" label={t.app.viewOnSite} icon={ArrowSquareOut} />}
185
186
  </>
@@ -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(() => {