@morphemecms/cms 0.9.0-next-20261105115842 → 0.9.1

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 (128) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/defaults/collections.ts +112 -58
  3. package/defaults/company.ts +1 -1
  4. package/defaults/editor.ts +2 -2
  5. package/defaults/texts.ts +14 -5
  6. package/demo/src/content/pages/app-store.md +0 -1
  7. package/demo/src/content/pages/blocks.md +0 -1
  8. package/icons/lucide/arrows-horizontal.svg +1 -0
  9. package/icons/lucide/columns-plus-left.svg +1 -0
  10. package/icons/lucide/columns-plus-right.svg +1 -0
  11. package/icons/lucide/file.svg +1 -0
  12. package/icons/material-symbols/arrows-horizontal.svg +1 -0
  13. package/icons/material-symbols/columns-plus-left.svg +1 -0
  14. package/icons/material-symbols/columns-plus-right.svg +1 -0
  15. package/icons/material-symbols/file.svg +1 -0
  16. package/icons/phosphor/arrows-horizontal.svg +1 -0
  17. package/icons/phosphor/columns-plus-left.svg +1 -0
  18. package/icons/phosphor/columns-plus-right.svg +1 -0
  19. package/icons/phosphor/file.svg +1 -0
  20. package/lib/fold-open.d.mts +2 -0
  21. package/lib/fold-open.mjs +22 -0
  22. package/lib/fold-open.test.mjs +24 -0
  23. package/lib/menu.test.mjs +16 -3
  24. package/lib/page-template.d.mts +54 -0
  25. package/lib/page-template.mjs +126 -0
  26. package/lib/page-template.test.mjs +129 -0
  27. package/lib/side-block.d.mts +2 -16
  28. package/lib/side-block.mjs +12 -30
  29. package/lib/side-block.test.mjs +13 -30
  30. package/package.json +1 -1
  31. package/scripts/check-side.mjs +49 -22
  32. package/scripts/editor-roundtrip.mjs +15 -6
  33. package/scripts/fixtures/side/side-full.md +8 -0
  34. package/scripts/fixtures/side/side-left.md +8 -0
  35. package/scripts/fixtures/side/side-none.md +7 -0
  36. package/scripts/fixtures/side/side-right.md +8 -0
  37. package/scripts/fixtures/side/side-single.md +8 -0
  38. package/scripts/side-width.mjs +29 -0
  39. package/scripts/side-width.test.mjs +30 -0
  40. package/site/content.config.ts +3 -4
  41. package/site/header.d.mts +21 -0
  42. package/site/header.mjs +35 -0
  43. package/site/header.test.mjs +59 -0
  44. package/site/icons.d.mts +16 -0
  45. package/site/icons.mjs +8 -0
  46. package/site/site.ts +9 -4
  47. package/site/templates.ts +7 -0
  48. package/site/theme.mjs +3 -0
  49. package/site/tracking.client.ts +4 -4
  50. package/src/App.tsx +12 -24
  51. package/src/AppList.tsx +36 -14
  52. package/src/AppSettingsTab.tsx +5 -3
  53. package/src/BlockDialog.tsx +50 -5
  54. package/src/DataFileFields.tsx +35 -0
  55. package/src/DataForm.tsx +16 -38
  56. package/src/Deleted.tsx +7 -3
  57. package/src/EntryForm.tsx +41 -88
  58. package/src/FooterSettings.tsx +76 -0
  59. package/src/FormFrame.tsx +2 -1
  60. package/src/HeaderSettings.tsx +100 -0
  61. package/src/Library.tsx +26 -14
  62. package/src/MarkdownEditor.tsx +18 -36
  63. package/src/Media.tsx +6 -3
  64. package/src/Redirects.tsx +12 -9
  65. package/src/SectionSettings.tsx +21 -0
  66. package/src/SectionSettingsIcon.tsx +10 -0
  67. package/src/SettingsSections.tsx +57 -0
  68. package/src/ThemeStore.tsx +28 -9
  69. package/src/Versions.tsx +3 -15
  70. package/src/busyAction.d.mts +3 -0
  71. package/src/busyAction.mjs +8 -0
  72. package/src/busyAction.test.mjs +16 -0
  73. package/src/dataFileValues.ts +30 -0
  74. package/src/editor.css +42 -3
  75. package/src/fields.tsx +29 -10
  76. package/src/footerPreview.d.mts +46 -0
  77. package/src/footerPreview.mjs +84 -0
  78. package/src/footerPreview.test.mjs +100 -0
  79. package/src/formSections.ts +100 -0
  80. package/src/frontmatter.mjs +1 -1
  81. package/src/frontmatter.test.mjs +11 -11
  82. package/src/headerPreview.d.mts +48 -0
  83. package/src/headerPreview.mjs +168 -0
  84. package/src/headerPreview.test.mjs +183 -0
  85. package/src/headerTheme.tsx +58 -0
  86. package/src/i18n/da-DK/common.ts +1 -0
  87. package/src/i18n/da-DK/library.ts +2 -0
  88. package/src/i18n/da-DK/preview.ts +2 -0
  89. package/src/i18n/da-DK/text.ts +1 -0
  90. package/src/i18n/en-US/common.ts +1 -0
  91. package/src/i18n/en-US/library.ts +4 -0
  92. package/src/i18n/en-US/preview.ts +3 -0
  93. package/src/i18n/en-US/text.ts +2 -1
  94. package/src/images.ts +7 -2
  95. package/src/menuChildren.ts +1 -3
  96. package/src/movedRoutes.d.mts +3 -0
  97. package/src/movedRoutes.mjs +35 -0
  98. package/src/movedRoutes.test.mjs +22 -0
  99. package/src/pagePreview.tsx +40 -29
  100. package/src/partPreview.tsx +59 -0
  101. package/src/schema.ts +7 -1
  102. package/src/sheetPlacement.d.mts +3 -0
  103. package/src/sheetPlacement.mjs +9 -0
  104. package/src/sheetPlacement.test.mjs +23 -0
  105. package/src/standard/views.ts +3 -3
  106. package/src/templateField.ts +28 -0
  107. package/src/themes.ts +3 -0
  108. package/src/ui.tsx +35 -8
  109. package/src/useDataFiles.ts +94 -0
  110. package/starter/package.json +1 -1
  111. package/theme/components/Footer.astro +14 -7
  112. package/theme/components/Header.astro +58 -33
  113. package/theme/components/MobileNav.astro +5 -4
  114. package/theme/components/RelatedPosts.astro +1 -1
  115. package/theme/components/headerParts.ts +17 -0
  116. package/theme/layouts/PageLayout.astro +20 -16
  117. package/theme/styles/base.css +6 -5
  118. package/theme/styles/blocks.css +8 -12
  119. package/theme/styles/components.css +8 -1
  120. package/theme/styles/global.css +5 -9
  121. package/theme/styles/ui.css +111 -5
  122. package/theme/theme.json +1 -0
  123. package/theme/ui/BusyContent.tsx +16 -0
  124. package/theme/ui/Button.tsx +25 -5
  125. package/theme/ui/IconButton.tsx +17 -4
  126. package/scripts/fixtures/side/side-off.md +0 -8
  127. package/scripts/fixtures/side/side-plain.md +0 -7
  128. package/scripts/fixtures/side/side-wide.md +0 -8
package/CHANGELOG.md CHANGED
@@ -1,14 +1,31 @@
1
1
  # @morphemecms/cms
2
2
 
3
- ## 0.9.0-next-20261105115842
3
+ ## 0.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2e5bc1d: Sites type-check again: the header settings screen passes its theme the right data.
8
+
9
+ ## 0.9.0
4
10
 
5
11
  ### Minor Changes
6
12
 
7
13
  - 34d7f2f: An installed app's page has the tabs Settings and Information.
14
+ - a5f16e1: Show block and section settings in a side panel on large screens.
15
+ - 995f08e: Buttons and icon buttons show the M3 loading indicator while their action runs.
16
+ - 9b1d632: An optional `src/data/header.json` adds a header action button (in place of the phone icon on larger screens) and sets the menu button's side; the new Appearance › Header and Appearance › Footer screens edit all header and footer settings with a live preview on a phone and a computer.
17
+ - facf878: A page chooses a template from the ones its theme offers (`template` in the front matter, "templates" in theme.json: single, full, sidebar-left, sidebar-right). The page flags `wide` and `side` are gone. A site must migrate its `wide: true` pages (for example to `template: single`) and may remove `side: false`.
8
18
 
9
19
  ### Patch Changes
10
20
 
21
+ - 09924d5: Install, update and uninstall buttons show the loading indicator while they run.
22
+ - 3a40970: The Save and "More versions" buttons use the busy button with the loading indicator.
23
+ - e1a9843: Media, redirect, restore, theme and move buttons show the loading indicator while they run.
11
24
  - 46636dc: Buttons in the editor are as wide as their label on every screen (Material 3), never across the screen.
25
+ - c22e682: Cards side by side or under each other keep 8dp between them, set by the new token `--card-gap`.
26
+ - 9047684: Cards in the Cards block rise to the first elevation when pointed at, as Material 3's filled cards do.
27
+ - 22d9179: Cards have 16dp of room inside them, as Material 3 has, and a card's picture follows that room.
28
+ - a9e9e95: Cards that link are tinted when pointed at or focused, as in Material 3, and no longer get an accent outline.
12
29
  - 35e53d0: The editor's Appstore and the cookie consent log no longer hang when an earlier request was cancelled.
13
30
  - 13998ad: Show the row that deletes what a screen shows (Delete promotion, Delete page …) in the theme's error colour.
14
31
  - 5c4fa60: Show the signed-in user and the site's domain at the top of the main drawer.
@@ -17,9 +34,15 @@
17
34
  - fc064fe: Align the main drawer header with the drawer's icons and arrows.
18
35
  - 2c6ee27: Show a divider under the main drawer's header row.
19
36
  - 152fb04: Make the main drawer's header row as high as a drawer item.
37
+ - 7d1f31b: The text toolbar has round corners and room all round it, also while it sticks under the top bar.
20
38
  - c19ddf7: On a phone, hide the top bar's side panel button on screens that show a back arrow.
21
39
  - 5db8ddc: Give every list item headline Material 3's label text size and weight.
40
+ - efbac0c: Lists in a page's dotted section frame keep their indent, so the bullets stand clear of the line.
41
+ - 86b0867: The "on this page" card on large screens is as wide as the navigation drawer.
42
+ - 5005d1f: Replace the section colour menu with a section settings sheet.
43
+ - eeb55b6: The template single draws its blocks across the column again and shows its first paragraph as a lead; the new `--type-lead-*` tokens (default: title large) let a theme style every lead.
22
44
  - 46b7672: The Appstore no longer waits for ever when the store does not answer. After 15 seconds it says "Can't reach the store. Try again in 10 minutes."
45
+ - 0210960: The update message stays while an update is saved.
23
46
 
24
47
  ## 0.8.0
25
48
 
@@ -6,12 +6,56 @@
6
6
  // block "Udtalelser"'s, packages/editor/src/standard/testimonials/list.ts, an app's lists its own,
7
7
  // src/apps/<app>/lists.ts).
8
8
  // files: single .json files: the settings (the standard pages …), the footer and the menu.
9
- import { AppWindow, SquareHalfBottom } from '@phosphor-icons/react';
9
+ import { AppWindow, ArrowsHorizontal, ColumnsPlusLeft, ColumnsPlusRight, File, SquareHalfBottom } from '@phosphor-icons/react';
10
10
  import type { Collection, Field } from '../src/schema';
11
11
  import { fromContactBar } from '../lib/contactBar';
12
12
  import { config } from '@site/config';
13
13
  import { standardPaths } from '@site/standardPages';
14
14
 
15
+ // The fields of one button of the action bar (the bottom bar on a phone, ActionBar.astro) and of the button in the header
16
+ // ("Headerknap", src/data/header.json): its text, where it leads, and the promotion or page it leads to.
17
+ const barButtonLabel: Field = {
18
+ name: 'label',
19
+ label: 'Knaptekst',
20
+ type: 'string',
21
+ max: 30,
22
+ hint: 'Hvis feltet er tomt, bruges standardteksten eller sidens navn.',
23
+ };
24
+
25
+ const barButtonTo: Field = {
26
+ name: 'to',
27
+ label: 'Fører til',
28
+ type: 'select',
29
+ required: true,
30
+ default: 'booking',
31
+ options: [
32
+ { label: 'Book en tid', value: 'booking' },
33
+ { label: 'Ring', value: 'phone' },
34
+ { label: 'Åbn promovering', value: 'promotion', onPage: 'promotions' },
35
+ { label: 'En side', value: 'page' },
36
+ ],
37
+ };
38
+
39
+ const barButtonOpen: Field = {
40
+ name: 'open',
41
+ label: 'Promovering',
42
+ type: 'relation',
43
+ to: 'promotions',
44
+ onPage: true,
45
+ required: true,
46
+ only: { field: 'to', is: ['promotion'] },
47
+ };
48
+
49
+ const barButtonHref: Field = {
50
+ name: 'href',
51
+ label: 'Side',
52
+ type: 'relation',
53
+ to: 'pages',
54
+ fixed: true,
55
+ required: true,
56
+ only: { field: 'to', is: ['page'] },
57
+ };
58
+
15
59
  // The page Google should count as the original of a page or a post (rel=canonical); empty is itself.
16
60
  // Under "Søgemaskiner" with the Google texts (EntryForm.tsx).
17
61
  const canonical: Field = {
@@ -136,7 +180,7 @@ const keywords: Field[] = [
136
180
  // The business, on "Virksomheden" (CompanyForm.tsx): its name and title, phone, e-mail, address and CVR number
137
181
  // (src/data/contact.json), shown at the top and bottom of every page and in the blocks that show them ("Kontakt" and
138
182
  // "Adresse"); and the bank details (src/data/payment.json), shown in "Betaling". The calendar
139
- // is the app Calendly's (src/apps/calendly/).
183
+ // is the app Calendly's (morpheme-apps/apps/calendly/).
140
184
  export const contactFields: Field[] = [
141
185
  { name: 'name', label: 'Navn', type: 'string', required: true },
142
186
  { name: 'role', label: 'Titel', type: 'string', required: true },
@@ -407,22 +451,20 @@ export const collections: Collection[] = [
407
451
  hint: 'Vises ikke på hjemmesiden.',
408
452
  },
409
453
  {
410
- name: 'wide',
411
- label: 'Hele bredden',
412
- type: 'boolean',
413
- layout: true,
414
- hint: 'Siden fylder hele bredden uden spalten ved siden af teksten.',
415
- },
416
- {
417
- name: 'side',
418
- label: 'Sidekolonne',
419
- type: 'boolean',
420
- toggle: ['Til', 'Fra'],
421
- // Without the key the column is on; only "Fra" is written (side: false).
422
- default: true,
423
- missing: true,
454
+ name: 'template',
455
+ label: 'Skabelon',
456
+ type: 'select',
457
+ segments: true,
424
458
  layout: true,
425
- hint: 'Blokken ved siden af teksten vises på siden. Den vælges under Udseende › Sidekolonne.',
459
+ // The options the theme in use offers, the first of its list the default (templateFieldOf in src/templateField.ts).
460
+ offered: 'templates',
461
+ options: [
462
+ { label: 'En kolonne', value: 'single', icon: File },
463
+ { label: 'Fuld bredde', value: 'full', icon: ArrowsHorizontal },
464
+ { label: 'Sidebjælke til venstre', value: 'sidebar-left', icon: ColumnsPlusLeft },
465
+ { label: 'Sidebjælke til højre', value: 'sidebar-right', icon: ColumnsPlusRight },
466
+ ],
467
+ hint: 'Temaet bestemmer, hvilke skabeloner der findes.',
426
468
  },
427
469
  {
428
470
  name: 'review',
@@ -469,7 +511,7 @@ export const collections: Collection[] = [
469
511
  // src/data/popups/<slug>.json, read by packages/editor/site/popups.ts, on every page or those picked under "Sider".
470
512
  // The pop-ups are shown by packages/editor/theme/components/Popup.astro; a kind an app adds without a part
471
513
  // in the pop-up (src/apps/<app>/popups/View.tsx) shows itself (the app Calendly's booking sheet,
472
- // src/apps/calendly/, when a button opens it).
514
+ // morpheme-apps/apps/calendly/, when a button opens it).
473
515
  kind: 'folder',
474
516
  id: 'popups',
475
517
  create: 'Ny promovering',
@@ -695,46 +737,10 @@ export const collections: Collection[] = [
695
737
  hint: 'Står under overskriften.',
696
738
  only: { field: 'type', is: ['text'] },
697
739
  },
698
- {
699
- name: 'label',
700
- label: 'Knaptekst',
701
- type: 'string',
702
- max: 30,
703
- hint: 'Hvis feltet er tomt, bruges standardteksten eller sidens navn.',
704
- only: { field: 'type', is: ['button'] },
705
- },
706
- {
707
- name: 'to',
708
- label: 'Fører til',
709
- type: 'select',
710
- required: true,
711
- default: 'booking',
712
- only: { field: 'type', is: ['button'] },
713
- options: [
714
- { label: 'Book en tid', value: 'booking' },
715
- { label: 'Ring', value: 'phone' },
716
- { label: 'Åbn promovering', value: 'promotion', onPage: 'promotions' },
717
- { label: 'En side', value: 'page' },
718
- ],
719
- },
720
- {
721
- name: 'open',
722
- label: 'Promovering',
723
- type: 'relation',
724
- to: 'promotions',
725
- onPage: true,
726
- required: true,
727
- only: { field: 'to', is: ['promotion'] },
728
- },
729
- {
730
- name: 'href',
731
- label: 'Side',
732
- type: 'relation',
733
- to: 'pages',
734
- fixed: true,
735
- required: true,
736
- only: { field: 'to', is: ['page'] },
737
- },
740
+ { ...barButtonLabel, only: { field: 'type', is: ['button'] } },
741
+ { ...barButtonTo, only: { field: 'type', is: ['button'] } },
742
+ barButtonOpen,
743
+ barButtonHref,
738
744
  ],
739
745
  },
740
746
  ],
@@ -848,4 +854,52 @@ export const collections: Collection[] = [
848
854
  },
849
855
  ],
850
856
  },
857
+ {
858
+ kind: 'files',
859
+ id: 'headerknap',
860
+ label: 'Headerknap',
861
+ singular: 'headerknap',
862
+ files: [
863
+ // src/data/header.json, optional (a site without it has no button and the menu button at the start), read by the header
864
+ // (Header.astro). The button is one of the action bar's (barButtonLabel …).
865
+ {
866
+ file: 'src/data/header.json',
867
+ label: 'Headeren',
868
+ view: '/',
869
+ optional: true,
870
+ fields: [
871
+ {
872
+ name: 'button',
873
+ label: 'Handlingsknap',
874
+ type: 'object',
875
+ fields: [
876
+ {
877
+ name: 'enabled',
878
+ label: 'Aktiveret',
879
+ type: 'boolean',
880
+ toggle: ['Til', 'Fra'],
881
+ default: false,
882
+ hint: 'Knappen står i headeren.',
883
+ },
884
+ barButtonLabel,
885
+ barButtonTo,
886
+ barButtonOpen,
887
+ barButtonHref,
888
+ ],
889
+ },
890
+ {
891
+ name: 'menuSide',
892
+ label: 'Menuknappens placering',
893
+ type: 'select',
894
+ segments: true,
895
+ default: 'start',
896
+ options: [
897
+ { label: 'Venstre', value: 'start' },
898
+ { label: 'Højre', value: 'end' },
899
+ ],
900
+ },
901
+ ],
902
+ },
903
+ ],
904
+ },
851
905
  ];
@@ -2,7 +2,7 @@
2
2
  // (src/data/contact.json) and its bank details (src/data/payment.json, under "Betaling", where a site may add its
3
3
  // other ways to pay, and where the settings of the apps installed that are a way to pay come after, as MobilePay's),
4
4
  // used all over the site; and its social media (src/data/social.json): the profiles and the post's share buttons.
5
- // The logo and the icon are under "Udseende" (BrandForm.tsx); the link to the calendar in Calendly is the app Calendly's (src/apps/calendly/).
5
+ // The logo and the icon are under "Udseende" (BrandForm.tsx); the link to the calendar in Calendly is the app Calendly's (morpheme-apps/apps/calendly/).
6
6
  import { AddressBook, Bank, CreditCard, ShareNetwork } from '@phosphor-icons/react';
7
7
  import type { Part } from '../src/schema';
8
8
  import { bankFields, contactFields, socialFields } from '@site/collections';
@@ -193,5 +193,5 @@ export const settings: string[] = settingsIds;
193
193
  * bring ("Udtalelser") and the apps (their lists.ts). */
194
194
  export const lists: string[] = [];
195
195
 
196
- /** What is in the header on every page, on a screen of its own under "Header" (#header). */
197
- export const header = ['menu', 'mobilmenu'];
196
+ /** What is in the header on every page, on one screen under "Udseende" › "Header" (#udseende/header, HeaderSettings.tsx). */
197
+ export const header = ['menu', 'mobilmenu', 'headerknap'];
package/defaults/texts.ts CHANGED
@@ -26,11 +26,12 @@ export const texts: Record<string, Record<string, string>> = {
26
26
  'Skærmlæsere springer billedet over.': 'Screen readers skip the picture.',
27
27
  Til: 'On',
28
28
  Fra: 'Off',
29
- Sidekolonne: 'Side column',
30
- 'Blokken ved siden af teksten vises på siden. Den vælges under Udseende › Sidekolonne.':
31
- 'The block beside the text is shown on the page. It is chosen under Appearance › Side column.',
32
- 'Hele bredden': 'Full width',
33
- 'Siden fylder hele bredden uden spalten ved siden af teksten.': 'The page fills the whole width without the column beside the text.',
29
+ Skabelon: 'Template',
30
+ 'En kolonne': 'Single column',
31
+ 'Fuld bredde': 'Full width',
32
+ 'Sidebjælke til venstre': 'Sidebar left',
33
+ 'Sidebjælke til højre': 'Sidebar right',
34
+ 'Temaet bestemmer, hvilke skabeloner der findes.': 'The theme decides which templates there are.',
34
35
  Status: 'Status',
35
36
  Kladde: 'Draft',
36
37
  'Vises ikke på hjemmesiden.': 'Not shown on the website.',
@@ -273,6 +274,14 @@ export const texts: Record<string, Record<string, string>> = {
273
274
  Mobilmenuen: 'The mobile menu',
274
275
  'Punkterne står på en linje under headeren på en telefon og kan skubbes til siden.':
275
276
  'The items are on a line under the header on a phone and can be swiped sideways.',
277
+ Headerknap: 'Header button',
278
+ headerknap: 'header button',
279
+ Headeren: 'The header',
280
+ Handlingsknap: 'Action button',
281
+ 'Knappen står i headeren.': 'The button is in the header.',
282
+ 'Menuknappens placering': 'Menu button position',
283
+ Venstre: 'Left',
284
+ Højre: 'Right',
276
285
  Menupunkter: 'Menu items',
277
286
  menupunkt: 'menu item',
278
287
  Undermenu: 'Submenu',
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  title: "App store"
3
3
  description: "Add new features to your site with one tap: apps and themes from the app store."
4
- wide: true
5
4
  seoTitle: "App store | Morpheme"
6
5
  seoDescription: "Add booking, newsletter signups, content-based colors and new themes to your site with one tap. Every install can be undone."
7
6
  keyword: "app store"
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  title: "Blocks"
3
3
  description: "Every block you can build a page from in Morpheme, shown live."
4
- wide: true
5
4
  seoTitle: "Blocks | Morpheme"
6
5
  seoDescription: "Buttons, cards, points, steps, checklists, FAQs, price cards, testimonials and more: every Morpheme block, shown live."
7
6
  keyword: "blocks"
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="12" x="2" y="6" rx="2" /></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /> <path d="M9 3v18" /></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /> <path d="M15 3v18" /></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="12" height="20" x="6" y="2" rx="2" /></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M140-160q-24.75 0-42.37-17.63Q80-195.25 80-220v-520q0-24.75 17.63-42.38Q115.25-800 140-800h680q24.75 0 42.38 17.62Q880-764.75 880-740v520q0 24.75-17.62 42.37Q844.75-160 820-160H140Zm0-60h70v-520h-70v520Zm130 0h420v-520H270v520Zm480 0h70v-520h-70v520ZM270-740v520-520Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M180-120q-24.75 0-42.37-17.63Q120-155.25 120-180v-600q0-24.75 17.63-42.38Q155.25-840 180-840h600q24.75 0 42.38 17.62Q840-804.75 840-780v600q0 24.75-17.62 42.37Q804.75-120 780-120H180Zm147-60v-600H180v600h147Zm60 0h393v-600H387v600Zm-60 0H180h147Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M180-120q-24.75 0-42.37-17.63Q120-155.25 120-180v-600q0-24.75 17.63-42.38Q155.25-840 180-840h600q24.75 0 42.38 17.62Q840-804.75 840-780v600q0 24.75-17.62 42.37Q804.75-120 780-120H180Zm453-60h147v-600H633v600Zm-60 0v-600H180v600h393Zm60 0h147-147Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M740-80H220q-24 0-42-18t-18-42v-680q0-24 18-42t42-18h520q24 0 42 18t18 42v680q0 24-18 42t-42 18Zm-520-60h520v-680H220v680Zm0 0v-680 680Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M237.66,133.66l-32,32a8,8,0,0,1-11.32-11.32L212.69,136H43.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L43.31,120H212.69l-18.35-18.34a8,8,0,0,1,11.32-11.32l32,32A8,8,0,0,1,237.66,133.66Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,32H104A16,16,0,0,0,88,48V208a16,16,0,0,0,16,16h24a16,16,0,0,0,16-16V48A16,16,0,0,0,128,32Zm0,176H104V48h24ZM200,32H176a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h24a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H176V48h24ZM72,128a8,8,0,0,1-8,8H48v16a8,8,0,0,1-16,0V136H16a8,8,0,0,1,0-16H32V104a8,8,0,0,1,16,0v16H64A8,8,0,0,1,72,128Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M80,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H80a16,16,0,0,0,16-16V48A16,16,0,0,0,80,32Zm0,176H56V48H80ZM152,32H128a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h24a16,16,0,0,0,16-16V48A16,16,0,0,0,152,32Zm0,176H128V48h24Zm96-80a8,8,0,0,1-8,8H224v16a8,8,0,0,1-16,0V136H192a8,8,0,0,1,0-16h16V104a8,8,0,0,1,16,0v16h16A8,8,0,0,1,248,128Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"/></svg>
@@ -0,0 +1,2 @@
1
+ export function withOpen<T>(open: ReadonlySet<T>, id: T, single: boolean): ReadonlySet<T>;
2
+ export function onlyFirst<T>(open: ReadonlySet<T>, order: T[]): ReadonlySet<T>;
@@ -0,0 +1,22 @@
1
+ // Which of a form's folding sections are open (formSections.ts): pure, so it is tested (fold-open.test.mjs). Exposes
2
+ // withOpen and onlyFirst. No dependencies.
3
+
4
+ /**
5
+ * The sections open after one is opened: beside the others, or with `single` (one open at a time) in their place.
6
+ * @template T
7
+ * @param {ReadonlySet<T>} open
8
+ * @param {T} id
9
+ * @param {boolean} single
10
+ * @returns {ReadonlySet<T>}
11
+ */
12
+ export const withOpen = (open, id, single) => (single ? new Set([id]) : open.has(id) ? open : new Set([...open, id]));
13
+
14
+ /**
15
+ * Only the first of the open sections, in the form's order (`order`), when more are open (the window narrowed to one at a
16
+ * time); else the same.
17
+ * @template T
18
+ * @param {ReadonlySet<T>} open
19
+ * @param {T[]} order
20
+ * @returns {ReadonlySet<T>}
21
+ */
22
+ export const onlyFirst = (open, order) => (open.size > 1 ? new Set(order.filter((id) => open.has(id)).slice(0, 1)) : open);
@@ -0,0 +1,24 @@
1
+ // Tests lib/fold-open.mjs: opening a section beside the others or in their place, and only the first left open.
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { onlyFirst, withOpen } from './fold-open.mjs';
5
+
6
+ test('a section opens beside the others', () => {
7
+ assert.deepEqual([...withOpen(new Set(['a']), 'b', false)], ['a', 'b']);
8
+ });
9
+
10
+ test('an open section stays as it is', () => {
11
+ const open = new Set(['a']);
12
+ assert.equal(withOpen(open, 'a', false), open);
13
+ });
14
+
15
+ test('one at a time: a section opens in the place of the others', () => {
16
+ assert.deepEqual([...withOpen(new Set(['a', 'c']), 'b', true)], ['b']);
17
+ });
18
+
19
+ test('only the first open section in the order stays open', () => {
20
+ assert.deepEqual([...onlyFirst(new Set(['c', 'b']), ['a', 'b', 'c'])], ['b']);
21
+ const one = new Set(['c']);
22
+ assert.equal(onlyFirst(one, ['a', 'b', 'c']), one);
23
+ assert.equal(onlyFirst(new Set(), ['a']).size, 0);
24
+ });
package/lib/menu.test.mjs CHANGED
@@ -8,7 +8,7 @@ import { currentPart, menuTree, currentPlace } from './menu.mjs';
8
8
 
9
9
  const known = {
10
10
  lists: ['udtalelser', 'cookies', 'priser'],
11
- header: ['menu', 'mobilmenu'],
11
+ header: ['menu', 'mobilmenu', 'headerknap'],
12
12
  partScreens: ['privatliv'],
13
13
  cookies: 'cookies',
14
14
  store: 'indstillinger/appstore',
@@ -54,6 +54,7 @@ test('the cookie list is under Privacy, whatever the way to it', () => {
54
54
  test('the menus are under Header', () => {
55
55
  assert.equal(currentPart('src/data/menu.json', { ...known, open: 'menu' }), 'header');
56
56
  assert.equal(currentPart('liste/mobilmenu', known), 'header');
57
+ assert.equal(currentPart('src/data/header.json', { ...known, open: 'headerknap' }), 'header');
57
58
  });
58
59
 
59
60
  test('a screen under a part is that part', () => {
@@ -170,8 +171,8 @@ const hrefs = {
170
171
  filer: '#filer',
171
172
  'interne-links': '#seo/interne-links',
172
173
  videresendelser: '#seo/videresendelser',
173
- header: '#header',
174
- footer: '#src/data/footer.json',
174
+ header: '#udseende/header',
175
+ footer: '#udseende/footer',
175
176
  virksomheden: '#virksomheden',
176
177
  apps: '#apps',
177
178
  appstore: '#indstillinger/appstore',
@@ -260,6 +261,18 @@ test('currentPlace of a page being edited, a list, the SEO rating, an Appearance
260
261
  assert.deepEqual(currentPlace('src/data/cookie-list/a.json', menu, { ...menuKnown, open: 'cookies' }), { section: 'privatliv' });
261
262
  });
262
263
 
264
+ test('currentPlace of Appearance › Header and its menus is Global › Header', () => {
265
+ assert.deepEqual(currentPlace('udseende/header', menu, menuKnown), { section: 'globalt', item: 'header' });
266
+ assert.deepEqual(currentPlace('src/data/menu.json', menu, { ...menuKnown, open: 'menu' }), { section: 'globalt', item: 'header' });
267
+ });
268
+
269
+ test('currentPlace of Appearance › Footer is Global › Footer, a row without children', () => {
270
+ assert.deepEqual(currentPlace('udseende/footer', menu, menuKnown), { section: 'globalt', item: 'footer' });
271
+ const global = menu.find((entry) => entry.id === 'globalt');
272
+ assert.equal(global.items.find((item) => item.id === 'footer').children, undefined);
273
+ assert.equal(global.items.find((item) => item.id === 'header').children, undefined);
274
+ });
275
+
263
276
  const appsGroups = [{ id: 'apps', title: 'Apps', ids: ['apps', 'appstore'] }];
264
277
  const appsPartOf = (id) => ({ label: `L ${id}`, href: id === 'appstore' ? '#indstillinger/appstore' : '#apps', count: null });
265
278
  const appsOf = (list) => (id) => (id === 'apps' ? list.map((a) => ({ id: a, label: `A ${a}`, href: `#apps/${a}` })) : []);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Return the theme's `templates` list when templateProblems finds no problem and the list is not undefined;
3
+ * else a copy of STANDARD_TEMPLATES.
4
+ *
5
+ * @param {{ templates?: unknown } | null | undefined} theme
6
+ * @returns {string[]} A copy of the theme's valid template list or of STANDARD_TEMPLATES.
7
+ */
8
+ export function templatesOf(theme: {
9
+ templates?: unknown;
10
+ } | null | undefined): string[];
11
+ /**
12
+ * Return page.template when it is a string in offered; else offered[0].
13
+ *
14
+ * @param {{ template?: unknown } | null | undefined} page
15
+ * @param {string[]} offered
16
+ * @returns {string} The resolved template id.
17
+ */
18
+ export function templateOf(page: {
19
+ template?: unknown;
20
+ } | null | undefined, offered: string[]): string;
21
+ /**
22
+ * True for 'sidebar-left' and 'sidebar-right'; false for every other value.
23
+ *
24
+ * @param {unknown} template
25
+ * @returns {boolean} Whether the template is a sidebar template.
26
+ */
27
+ export function hasSidebar(template: unknown): boolean;
28
+ /**
29
+ * Return offered in the same order; without the sidebar templates when sideChosen is false.
30
+ *
31
+ * @param {string[]} offered
32
+ * @param {boolean} sideChosen
33
+ * @returns {string[]} A new array of template choices.
34
+ */
35
+ export function templateChoices(offered: string[], sideChosen: boolean): string[];
36
+ /**
37
+ * Problems with a theme.json `templates` value, one English sentence each.
38
+ * [] when the value is undefined or valid.
39
+ *
40
+ * @param {unknown} value
41
+ * @returns {string[]} List of problem descriptions.
42
+ */
43
+ export function templateProblems(value: unknown): string[];
44
+ /**
45
+ * What the form writes to the front matter for the chosen template: '' (no key) for the theme's default (offered[0]),
46
+ * and for a page with none; else the template as it is.
47
+ *
48
+ * @param {unknown} value
49
+ * @param {string[]} offered
50
+ * @returns {unknown} '' or the value to write.
51
+ */
52
+ export function templateWritten(value: unknown, offered: string[]): unknown;
53
+ export const PAGE_TEMPLATES: string[];
54
+ export const STANDARD_TEMPLATES: string[];