@morphemecms/cms 0.13.0 → 0.14.0-next-20261107093528

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 (166) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/defaults/editor.ts +3 -14
  3. package/icons/lucide/dots-three-vertical.svg +1 -0
  4. package/icons/material-symbols/dots-three-vertical.svg +1 -0
  5. package/icons/phosphor/dots-three-vertical.svg +1 -0
  6. package/lib/appChanges.d.mts +16 -0
  7. package/lib/appChanges.mjs +20 -0
  8. package/lib/appChanges.test.mjs +30 -0
  9. package/lib/apps.d.mts +2 -2
  10. package/lib/apps.mjs +13 -7
  11. package/lib/appsFile.d.mts +20 -0
  12. package/lib/appsFile.mjs +57 -0
  13. package/lib/appsFile.test.mjs +53 -0
  14. package/lib/build-settings.mjs +1 -1
  15. package/lib/changelog.d.mts +7 -0
  16. package/lib/changelog.mjs +16 -5
  17. package/lib/footerClearance.d.mts +1 -0
  18. package/lib/footerClearance.mjs +9 -0
  19. package/lib/footerClearance.test.mjs +24 -0
  20. package/lib/footerLinks.d.mts +12 -0
  21. package/lib/footerLinks.mjs +26 -0
  22. package/lib/footerLinks.test.mjs +32 -0
  23. package/lib/google-result.test.mjs +40 -9
  24. package/lib/menu.mjs +1 -1
  25. package/lib/menu.test.mjs +9 -8
  26. package/lib/releaseNotes.d.mts +136 -0
  27. package/lib/releaseNotes.mjs +249 -0
  28. package/lib/releaseNotes.test.mjs +267 -0
  29. package/lib/serviceFeed.d.mts +2 -0
  30. package/lib/serviceFeed.mjs +13 -0
  31. package/lib/serviceFeed.test.mjs +53 -0
  32. package/lib/serviceStatus.d.mts +72 -0
  33. package/lib/serviceStatus.mjs +143 -0
  34. package/lib/serviceStatus.test.mjs +136 -0
  35. package/lib/strayDividers.d.mts +6 -0
  36. package/lib/strayDividers.mjs +15 -0
  37. package/lib/strayDividers.test.mjs +25 -0
  38. package/package.json +5 -3
  39. package/release-notes/i18n/da-DK/pending/apps-turn-off.json +6 -0
  40. package/release-notes/i18n/da-DK/pending/apps-turn-off.webp +0 -0
  41. package/release-notes/i18n/da-DK/pending/developer-menu.json +5 -0
  42. package/release-notes/i18n/da-DK/pending/dynamic-lists-page.json +5 -0
  43. package/release-notes/i18n/da-DK/pending/editor-footer.json +6 -0
  44. package/release-notes/i18n/da-DK/pending/editor-footer.webp +0 -0
  45. package/release-notes/i18n/da-DK/pending/editor-toolbar-dock.json +5 -0
  46. package/release-notes/i18n/da-DK/pending/google-preview-closer.json +6 -0
  47. package/release-notes/i18n/da-DK/pending/google-preview-closer.webp +0 -0
  48. package/release-notes/i18n/da-DK/pending/morpheme-connection-page.json +5 -0
  49. package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.json +6 -0
  50. package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.webp +0 -0
  51. package/release-notes/i18n/en-US/pending/apps-turn-off.json +6 -0
  52. package/release-notes/i18n/en-US/pending/apps-turn-off.webp +0 -0
  53. package/release-notes/i18n/en-US/pending/developer-menu.json +5 -0
  54. package/release-notes/i18n/en-US/pending/dynamic-lists-page.json +5 -0
  55. package/release-notes/i18n/en-US/pending/editor-footer.json +6 -0
  56. package/release-notes/i18n/en-US/pending/editor-footer.webp +0 -0
  57. package/release-notes/i18n/en-US/pending/editor-toolbar-dock.json +5 -0
  58. package/release-notes/i18n/en-US/pending/google-preview-closer.json +6 -0
  59. package/release-notes/i18n/en-US/pending/google-preview-closer.webp +0 -0
  60. package/release-notes/i18n/en-US/pending/morpheme-connection-page.json +5 -0
  61. package/release-notes/i18n/en-US/pending/quiet-lamps-wander.json +6 -0
  62. package/release-notes/i18n/en-US/pending/quiet-lamps-wander.webp +0 -0
  63. package/scripts/app-screenshots.mjs +4 -4
  64. package/scripts/build-settings-problems.mjs +1 -1
  65. package/scripts/built-site.mjs +1 -1
  66. package/scripts/check-content.mjs +1 -1
  67. package/scripts/check-release-notes.mjs +12 -0
  68. package/scripts/check-worker.mjs +1 -1
  69. package/scripts/editor-screens.mjs +32 -16
  70. package/scripts/release-notes-pending.mjs +46 -0
  71. package/scripts/release-notes.mjs +39 -0
  72. package/site/apps.ts +10 -4
  73. package/site/consent.ts +3 -3
  74. package/site/defineSite.mjs +1 -1
  75. package/site/icons.d.mts +4 -0
  76. package/site/icons.mjs +2 -0
  77. package/site/pageColors.ts +3 -3
  78. package/site/siteCsp.d.mts +1 -1
  79. package/site/siteCsp.mjs +3 -2
  80. package/src/ActionParts.tsx +1 -1
  81. package/src/Advanced.tsx +4 -4
  82. package/src/App.tsx +22 -17
  83. package/src/AppEnabledSwitch.tsx +21 -0
  84. package/src/AppList.tsx +47 -19
  85. package/src/AppSettingsTab.tsx +9 -5
  86. package/src/BlockParts.tsx +7 -7
  87. package/src/BlockSettings.tsx +3 -3
  88. package/src/CmsPage.tsx +11 -10
  89. package/src/Colors.tsx +2 -2
  90. package/src/CommunicationParts.tsx +18 -2
  91. package/src/ContainmentParts.tsx +1 -1
  92. package/src/DataForm.tsx +1 -0
  93. package/src/EditorFooter.tsx +19 -9
  94. package/src/EntryForm.tsx +1 -0
  95. package/src/FooterCopyright.tsx +7 -0
  96. package/src/FooterDivider.tsx +5 -0
  97. package/src/FooterLink.tsx +18 -0
  98. package/src/FooterLinks.tsx +34 -0
  99. package/src/Library.tsx +21 -10
  100. package/src/MainDrawerParts.tsx +1 -1
  101. package/src/MarkdownEditor.tsx +1 -1
  102. package/src/ModalSheetSample.tsx +1 -1
  103. package/src/MorphemeConnection.tsx +30 -0
  104. package/src/MorphemeLogo.tsx +9 -0
  105. package/src/NavigationParts.tsx +1 -1
  106. package/src/NavigationRestParts.tsx +1 -1
  107. package/src/Parts.tsx +9 -9
  108. package/src/PartsShown.tsx +1 -1
  109. package/src/PickerParts.tsx +1 -1
  110. package/src/ReleaseNoteCard.tsx +17 -0
  111. package/src/ReleaseNotes.tsx +52 -0
  112. package/src/Screen.tsx +21 -12
  113. package/src/SearchPreview.tsx +37 -14
  114. package/src/SelectionParts.tsx +1 -1
  115. package/src/ServiceStatus.tsx +59 -0
  116. package/src/SideColumn.tsx +2 -2
  117. package/src/StoreConnectNotice.tsx +1 -1
  118. package/src/StoreConnectedRow.tsx +20 -14
  119. package/src/StoreStatus.tsx +2 -10
  120. package/src/ThemeStoreScreen.tsx +1 -2
  121. package/src/Tokens.tsx +2 -2
  122. package/src/apps.ts +16 -14
  123. package/src/buildSettings.ts +1 -1
  124. package/src/cmsView.d.mts +1 -0
  125. package/src/cmsView.mjs +4 -2
  126. package/src/cmsView.test.mjs +6 -0
  127. package/src/editor.css +13 -2
  128. package/src/examples.ts +2 -2
  129. package/src/fabPresence.tsx +24 -0
  130. package/src/googleResult.d.mts +12 -1
  131. package/src/googleResult.mjs +19 -2
  132. package/src/i18n/da-DK/apps.ts +5 -1
  133. package/src/i18n/da-DK/cms.ts +3 -0
  134. package/src/i18n/da-DK/common.ts +3 -2
  135. package/src/i18n/da-DK/footer.ts +26 -0
  136. package/src/i18n/da-DK/index.ts +2 -0
  137. package/src/i18n/da-DK/parts.ts +5 -0
  138. package/src/i18n/en-US/advanced.ts +1 -1
  139. package/src/i18n/en-US/apps.ts +6 -1
  140. package/src/i18n/en-US/cms.ts +5 -2
  141. package/src/i18n/en-US/common.ts +6 -4
  142. package/src/i18n/en-US/footer.ts +24 -0
  143. package/src/i18n/en-US/index.ts +2 -0
  144. package/src/i18n/en-US/parts.ts +8 -3
  145. package/src/menuChildren.ts +6 -14
  146. package/src/movedRoutes.mjs +12 -8
  147. package/src/movedRoutes.test.mjs +13 -10
  148. package/src/r2Caching.ts +1 -1
  149. package/src/registry.tsx +2 -2
  150. package/src/schema.ts +1 -1
  151. package/src/site.ts +2 -2
  152. package/src/statusReport.ts +24 -0
  153. package/src/store.ts +4 -8
  154. package/src/themeUi.ts +7 -7
  155. package/src/ui.tsx +4 -2
  156. package/src/useFooterClearance.ts +30 -0
  157. package/src/useStrayDividers.ts +24 -0
  158. package/theme/components/AppSlot.astro +4 -4
  159. package/theme/styles/global.css +10 -0
  160. package/theme/styles/ui.css +13 -0
  161. package/theme/ui/Badge.tsx +9 -3
  162. package/worker/cms-update.js +49 -12
  163. package/worker/index.js +10 -4
  164. package/worker/plan-status.js +1 -1
  165. package/worker/r2-status.js +1 -1
  166. package/worker/service-status.js +18 -0
@@ -28,5 +28,16 @@ export function searchedAs({ title, front, seoTitle, seoDescription, description
28
28
  title: string;
29
29
  description: string;
30
30
  };
31
- /** The address line of a Google result: the domain and the path's parts, joined by " › ". */
31
+ /** The address line of a Google result: "https://" and the domain, then the path's parts, joined by " › ". */
32
32
  export function searchCrumbs(domain: any, path: any): string;
33
+ /**
34
+ * Splits `value` into parts so that each occurrence of `keyword` (case-insensitive, literal) is a bold part, as Google
35
+ * bolds the searched words in a description. The original case is kept. A blank keyword gives one plain part.
36
+ * @param {string} value
37
+ * @param {string} [keyword]
38
+ * @returns {{ text: string; bold: boolean }[]}
39
+ */
40
+ export function boldParts(value: string, keyword?: string): {
41
+ text: string;
42
+ bold: boolean;
43
+ }[];
@@ -30,10 +30,27 @@ export function searchedAs({ title, front = false, seoTitle, seoDescription, des
30
30
  return { title: resultTitle, description: resultDescription };
31
31
  }
32
32
 
33
- /** The address line of a Google result: the domain and the path's parts, joined by " › ". */
33
+ /** The address line of a Google result: "https://" and the domain, then the path's parts, joined by " › ". */
34
34
  export function searchCrumbs(domain, path) {
35
35
  const parts = String(path ?? '')
36
36
  .split('/')
37
37
  .filter((part) => part !== '');
38
- return [domain, ...parts].join(SEPARATOR);
38
+ return [`https://${domain}`, ...parts].join(SEPARATOR);
39
+ }
40
+
41
+ /**
42
+ * Splits `value` into parts so that each occurrence of `keyword` (case-insensitive, literal) is a bold part, as Google
43
+ * bolds the searched words in a description. The original case is kept. A blank keyword gives one plain part.
44
+ * @param {string} value
45
+ * @param {string} [keyword]
46
+ * @returns {{ text: string; bold: boolean }[]}
47
+ */
48
+ export function boldParts(value, keyword) {
49
+ const word = text(keyword);
50
+ if (!word) return [{ text: value, bold: false }];
51
+ const pattern = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
52
+ return value
53
+ .split(pattern)
54
+ .map((part, i) => ({ text: part, bold: i % 2 === 1 }))
55
+ .filter((part) => part.text !== '');
39
56
  }
@@ -3,6 +3,8 @@ import type { Messages } from '../en-US';
3
3
  export const apps: Messages['apps'] = {
4
4
  installed: 'Installeret',
5
5
  notInstalled: 'Ikke installeret',
6
+ enabled: 'Aktiveret',
7
+ deactivated: 'Deaktiveret',
6
8
  publisher: (name) => `Udvikler: ${name}`,
7
9
  screenshots: 'Skærmbilleder',
8
10
  about: 'Om appen',
@@ -46,7 +48,7 @@ export const apps: Messages['apps'] = {
46
48
  notUsed: 'Bruges ingen steder',
47
49
  stuck: 'Appen bruges og kan ikke afinstalleres. Fjern den fra stederne ovenfor først.',
48
50
  blocks: 'Blokke',
49
- lists: 'Lister',
51
+ lists: 'Dynamiske lister',
50
52
  tabSettings: 'Indstillinger',
51
53
  tabInfo: 'Information',
52
54
  tabs: 'Appens dele',
@@ -94,6 +96,8 @@ export const apps: Messages['apps'] = {
94
96
  commit: {
95
97
  install: (app) => `Editor: installer appen ${app}`,
96
98
  uninstall: (app) => `Editor: afinstaller appen ${app}`,
99
+ turnOn: (app) => `Editor: slå appen ${app} til`,
100
+ turnOff: (app) => `Editor: slå appen ${app} fra`,
97
101
  update: (app, version) => `Editor: opdater appen ${app} til ${version}`,
98
102
  },
99
103
  };
@@ -7,6 +7,9 @@ export const cms: Messages['cms'] = {
7
7
  upToDate: 'Opdateret',
8
8
  available: (v) => `Version ${v} er klar`,
9
9
  whatsNew: 'Nyheder',
10
+ newInYourVersion: 'Nyt i din version',
11
+ alsoInThisVersion: 'Også i denne version',
12
+ storeConnection: 'Forbindelse til Appstore',
10
13
  major: 'En ny hovedversion installeres ikke af sig selv.',
11
14
  skipped: (v) => `Version ${v} blev fortrudt, fordi hjemmesiden ikke kunne opdateres med den.`,
12
15
  needsNpm: 'Opdatering kræver npm. Kør dette i hjemmesidens repo:',
@@ -91,7 +91,7 @@ export const common: Messages['common'] = {
91
91
  company: 'Virksomheden',
92
92
  header: 'Header',
93
93
  footer: 'Footer',
94
- lists: 'Lister',
94
+ lists: 'Dynamiske lister',
95
95
  blocks: 'Blokskabeloner',
96
96
  media: 'Filer',
97
97
  appearance: 'Udseende',
@@ -110,7 +110,8 @@ export const common: Messages['common'] = {
110
110
  redirects: 'Videresendelser',
111
111
  account: 'Konto',
112
112
  settings: 'Indstillinger',
113
- advanced: 'Avanceret',
113
+ developer: 'Udvikler',
114
+ caching: 'Caching og builds',
114
115
  licenses: 'Licenser',
115
116
  morpheme: 'Morpheme',
116
117
  },
@@ -0,0 +1,26 @@
1
+ // What the editor's footer and its "System status" screen say (EditorFooter.tsx, ServiceStatus.tsx).
2
+ import type { Messages } from '../en-US';
3
+
4
+ export const footer: Messages['footer'] = {
5
+ support: 'Support',
6
+ status: 'Driftsstatus',
7
+ terms: 'Vilkår',
8
+ security: 'Rapportér sikkerhedsfejl',
9
+ privacy: 'Privatlivspolitik',
10
+ openStatusPage: 'Åbn statusside',
11
+ loading: 'Tjekker status…',
12
+ state: {
13
+ ok: 'Alle systemer virker',
14
+ minor: 'Nogle systemer er langsomme',
15
+ major: 'Nogle systemer er nede',
16
+ unknown: 'Kan ikke tjekke status',
17
+ },
18
+ component: {
19
+ operational: 'Virker',
20
+ degraded_performance: 'Langsom',
21
+ under_maintenance: 'Vedligeholdes',
22
+ partial_outage: 'Delvist nede',
23
+ major_outage: 'Nede',
24
+ unknown: 'Kan ikke tjekkes',
25
+ },
26
+ };
@@ -12,6 +12,7 @@ import { common } from './common';
12
12
  import { company } from './company';
13
13
  import { consents } from './consents';
14
14
  import { forms } from './forms';
15
+ import { footer } from './footer';
15
16
  import { history } from './history';
16
17
  import { keywords } from './keywords';
17
18
  import { licenses } from './licenses';
@@ -39,6 +40,7 @@ export const daDK: Messages = {
39
40
  colors,
40
41
  redirects,
41
42
  consents,
43
+ footer,
42
44
  seo,
43
45
  keywords,
44
46
  licenses,
@@ -635,6 +635,11 @@ export const parts: Messages['parts'] = {
635
635
  number: 'Antal',
636
636
  many: 'Mere end 999',
637
637
  onIconButton: 'På en ikonknap',
638
+ statusTones: 'Statustoner',
639
+ toneOk: 'Virker',
640
+ toneMinor: 'Langsom',
641
+ toneMajor: 'Delvist nede',
642
+ toneUnknown: 'Kan ikke tjekkes',
638
643
  new: 'Nyt',
639
644
  count: (n) => `${n} nye`,
640
645
  messages: 'Beskeder',
@@ -1,4 +1,4 @@
1
- // "Avanceret" under "Indstillinger" (Advanced.tsx): the faster editor (a copy of the website's files in Cloudflare R2,
1
+ // "Caching and builds" under Developer (Advanced.tsx): the faster editor (a copy of the website's files in Cloudflare R2,
2
2
  // r2Caching.ts), and the switches for updating the website (buildSettings.ts).
3
3
  export const advanced = {
4
4
  r2Caching: 'Faster editor',
@@ -5,6 +5,9 @@ import type { AppHook } from '../../../lib/hooks';
5
5
  export const apps = {
6
6
  installed: 'Installed',
7
7
  notInstalled: 'Not installed',
8
+ /** The switch at the top of an installed app's Settings tab, and its status under its name while it is off. */
9
+ enabled: 'Enabled',
10
+ deactivated: 'Deactivated',
8
11
  /** Under its name on its page: who made it. */
9
12
  publisher: (name: string) => `Developer: ${name}`,
10
13
  /** On its page: its pictures (each opening it whole), what it does more fully, what it needs of the site
@@ -70,7 +73,7 @@ export const apps = {
70
73
  /** Over the blocks it adds, and the lists it brings ("Priser"); its kinds of the site's lists (a Mailchimp popup)
71
74
  * are under the list's name. */
72
75
  blocks: 'Blocks',
73
- lists: 'Lists',
76
+ lists: 'Dynamic lists',
74
77
  /** The tabs of an installed app's page, in the top bar: its settings, and what it says about itself. */
75
78
  tabSettings: 'Settings',
76
79
  tabInfo: 'Information',
@@ -140,6 +143,8 @@ export const apps = {
140
143
  commit: {
141
144
  install: (app: string) => `Editor: install the app ${app}`,
142
145
  uninstall: (app: string) => `Editor: uninstall the app ${app}`,
146
+ turnOn: (app: string) => `Editor: turn on the app ${app}`,
147
+ turnOff: (app: string) => `Editor: turn off the app ${app}`,
143
148
  update: (app: string, version: string) => `Editor: update the app ${app} to ${version}`,
144
149
  },
145
150
  };
@@ -1,5 +1,5 @@
1
- // "Morpheme" under "Indstillinger" (the CMS's update page, #morpheme): the running version, a new one and its notes,
2
- // and an update's progress; also the notifications and commit messages (worker/cms-update.js).
1
+ // "Morpheme" under "Indstillinger" (the CMS's update page, #morpheme): the running version, the notes of the update just
2
+ // installed, a new one and its notes, and an update's progress; also the notifications and commit messages (worker/cms-update.js).
3
3
 
4
4
  export const cms = {
5
5
  running: (v: string) => `Version ${v}`,
@@ -9,6 +9,9 @@ export const cms = {
9
9
  upToDate: 'Up to date',
10
10
  available: (v: string) => `Version ${v} is available`,
11
11
  whatsNew: 'What’s new',
12
+ newInYourVersion: 'New in your version',
13
+ alsoInThisVersion: 'Also in this version',
14
+ storeConnection: 'Store connection',
12
15
  major: 'A new major version isn’t installed by itself.',
13
16
  skipped: (v: string) => `Version ${v} was undone because the website couldn’t be updated with it.`,
14
17
  /** Followed by the command, in a code block. */
@@ -104,7 +104,7 @@ export const common = {
104
104
  company: 'Business',
105
105
  header: 'Header',
106
106
  footer: 'Footer',
107
- lists: 'Lists',
107
+ lists: 'Dynamic lists',
108
108
  blocks: 'Block templates',
109
109
  media: 'Files',
110
110
  /** How the site looks (#appearance, Library.tsx Theme): the theme, the colours, the logo and icon, and the site's own
@@ -125,7 +125,7 @@ export const common = {
125
125
  blockSettings: 'Block settings',
126
126
  /** The apps that come with the site (AppList.tsx, #apps). */
127
127
  apps: 'Apps',
128
- /** The parts the editor and the site are built of (#appearance/parts: Parts.tsx, BlockParts.tsx). */
128
+ /** The parts the editor and the site are built of (#developer/parts: Parts.tsx, BlockParts.tsx). */
129
129
  editorParts: 'Parts',
130
130
  /** Under Indstillinger: the redirects and the site's own parts (#seo, Library.tsx Seo). */
131
131
  seo: 'Search engine optimization',
@@ -133,8 +133,10 @@ export const common = {
133
133
  account: 'Account',
134
134
  /** The last section of the main drawer, and the first part of the path to a setting (placeOf in site.ts). */
135
135
  settings: 'Settings',
136
- /** Under Indstillinger: the faster editor (#advanced, Advanced.tsx). */
137
- advanced: 'Advanced',
136
+ /** Under Settings: the developer screens, the parts and the caching (#developer, Library.tsx Developer). */
137
+ developer: 'Developer',
138
+ /** Under Developer: the faster editor and the build settings (#developer/caching, Advanced.tsx). */
139
+ caching: 'Caching and builds',
138
140
  /** Under Indstillinger: the licences of what the site and the editor are made with (#licenses, Licenses.tsx). */
139
141
  licenses: 'Licenses',
140
142
  /** Under Indstillinger: the CMS's version and its updates (#morpheme). */
@@ -0,0 +1,24 @@
1
+ // What the editor's footer and its "System status" screen say (EditorFooter.tsx, ServiceStatus.tsx).
2
+ export const footer = {
3
+ support: 'Support',
4
+ status: 'System status',
5
+ terms: 'Terms of use',
6
+ security: 'Report security issues',
7
+ privacy: 'Privacy policy',
8
+ openStatusPage: 'Open status page',
9
+ loading: 'Checking the status…',
10
+ state: {
11
+ ok: 'All systems work',
12
+ minor: 'Some systems are slow',
13
+ major: 'Some systems are down',
14
+ unknown: 'Can’t check the status',
15
+ },
16
+ component: {
17
+ operational: 'Working',
18
+ degraded_performance: 'Slow',
19
+ under_maintenance: 'Under maintenance',
20
+ partial_outage: 'Partly down',
21
+ major_outage: 'Down',
22
+ unknown: 'Can’t check',
23
+ },
24
+ };
@@ -12,6 +12,7 @@ import { common } from './common';
12
12
  import { company } from './company';
13
13
  import { consents } from './consents';
14
14
  import { forms } from './forms';
15
+ import { footer } from './footer';
15
16
  import { history } from './history';
16
17
  import { keywords } from './keywords';
17
18
  import { licenses } from './licenses';
@@ -39,6 +40,7 @@ export const enUS = {
39
40
  colors,
40
41
  redirects,
41
42
  consents,
43
+ footer,
42
44
  seo,
43
45
  keywords,
44
46
  licenses,
@@ -1,4 +1,4 @@
1
- // "Dele" (#appearance/parts): the editor's parts (Parts.tsx), what each is for and the texts in the examples, and the standard
1
+ // "Parts" (#developer/parts): the editor's parts (Parts.tsx), what each is for and the texts in the examples, and the standard
2
2
  // blocks and the apps' (BlockParts.tsx), all with the made-up site of examples.ts.
3
3
  export const parts = {
4
4
  intro: (file: string) =>
@@ -253,7 +253,7 @@ export const parts = {
253
253
  /** What a kind of entry an app adds (a pop-up) is drawn with. */
254
254
  entry: { name: 'An example', heading: 'A heading', text: 'A short text that tells a little more.' },
255
255
  },
256
- /** "Dele" › "Værdier" (#appearance/parts/values, Tokens.tsx): the theme's design tokens, each with what it is used for. */
256
+ /** "Parts" › "Værdier" (#developer/parts/values, Tokens.tsx): the theme's design tokens, each with what it is used for. */
257
257
  tokens: {
258
258
  intro: 'The theme’s values for corners, shadows, typography, and colors, as the theme sets them.',
259
259
  groups: { shape: 'Corners', elevation: 'Shadows', type: 'Typography', color: 'Colors' },
@@ -355,7 +355,7 @@ export const parts = {
355
355
  /** A token the theme does not set, with the one it gets instead. */
356
356
  from: (token: string, value: string, from: string) => `${token}: ${value} (as ${from})`,
357
357
  },
358
- /** "Dele" (#appearance/parts) and its screens: the editor's parts, the theme's UI components, the blocks (the standard
358
+ /** "Parts" (#developer/parts) and its screens: the editor's parts, the theme's UI components, the blocks (the standard
359
359
  * ones and the apps') and the lists of kinds ("Promotion"). */
360
360
  screens: {
361
361
  editor: 'Editor',
@@ -659,6 +659,11 @@ export const parts = {
659
659
  number: 'Number',
660
660
  many: 'More than 999',
661
661
  onIconButton: 'On an icon button',
662
+ statusTones: 'Status tones',
663
+ toneOk: 'Working',
664
+ toneMinor: 'Slow',
665
+ toneMajor: 'Partly down',
666
+ toneUnknown: "Can't check",
662
667
  new: 'New',
663
668
  count: (n: number) => `${n} new`,
664
669
  messages: 'Messages',
@@ -1,27 +1,19 @@
1
- // The second level of the main menu (MainMenu.tsx, lib/menu.mjs menuTree): the lists under Lists,
2
- // the parts under Business, the apps installed under Apps and the two lists under Redirects. Exposes childrenOf.
3
- // Uses site.ts (collections, apps, companyParts), Library.tsx (hrefOf, listsShown) and Redirects.tsx (TAB_ROUTES).
4
- import { installedApps } from './apps';
1
+ // The second level of the main menu (MainMenu.tsx, lib/menu.mjs menuTree): the parts under Business,
2
+ // the active apps (installed, not turned off) under Apps and the two lists under Redirects. Exposes childrenOf.
3
+ // Uses site.ts (apps, companyParts), apps.ts (activeApps) and Redirects.tsx (TAB_ROUTES).
4
+ import { activeApps } from './apps';
5
5
  import { t } from './i18n';
6
- import { hrefOf } from './Library';
7
6
  import { TAB_ROUTES } from './Redirects';
8
7
  import type { RepoFile } from './github';
9
- import { apps, collections, companyParts, listsShown } from './site';
8
+ import { apps, companyParts } from './site';
10
9
 
11
10
  export type MenuChild = { id: string; label: string; href: string };
12
11
 
13
- const listOf = (ids: string[]): MenuChild[] =>
14
- ids.flatMap((id) => {
15
- const c = collections.find((x) => x.id === id);
16
- return c ? [{ id, label: c.label, href: hrefOf(c) }] : [];
17
- });
18
-
19
12
  /** The sub-items of a menu item, in their order: none for most. */
20
13
  export function childrenOf(id: string, files: RepoFile[]): MenuChild[] {
21
- if (id === 'lists') return listOf(listsShown(files));
22
14
  if (id === 'company') return companyParts.map((p) => ({ id: p.id, label: p.label, href: `#company/${p.id}` }));
23
15
  if (id === 'apps') {
24
- const installed = installedApps(files);
16
+ const installed = activeApps(files);
25
17
  return apps.filter((a) => installed.includes(a.id)).map((a) => ({ id: a.id, label: a.label, href: `#apps/${a.id}` }));
26
18
  }
27
19
  if (id === 'redirects') {
@@ -6,12 +6,14 @@ export const MOVED = /** @type {Record<string, string>} */ ({
6
6
  farver: 'appearance/colors',
7
7
  'virksomheden/logo': 'company/logo',
8
8
  'tema/blokvalg': 'block-settings',
9
- dele: 'appearance/parts',
9
+ dele: 'developer/parts',
10
10
  tema: 'appearance',
11
11
  'tema/farver': 'appearance/colors',
12
12
  'tema/logo': 'company/logo',
13
- 'tema/dele': 'appearance/parts',
14
- 'tema/dele/editor': 'appearance/parts/editor',
13
+ 'tema/dele': 'developer/parts',
14
+ 'tema/dele/editor': 'developer/parts/editor',
15
+ // "Advanced" is "Caching and builds" under "Developer" now.
16
+ advanced: 'developer/caching',
15
17
  'virksomheden/bank': 'company/payment/bank',
16
18
  videresendelser: 'seo/redirects',
17
19
  'videresendelser/alle': 'seo/redirects/all',
@@ -50,10 +52,12 @@ export const RENAMED = /** @type {Record<string, string>} */ ({
50
52
  'udseende/sidekolonne': 'appearance/side-column',
51
53
  'udseende/farver': 'appearance/colors',
52
54
  'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
53
- 'udseende/dele': 'appearance/parts',
54
- 'udseende/dele/blokke': 'appearance/parts/blocks',
55
- 'udseende/dele/liste': 'appearance/parts/list',
56
- 'udseende/dele/vaerdier': 'appearance/parts/values',
55
+ // "Parts" moved from Appearance to Developer, with all its screens.
56
+ 'appearance/parts': 'developer/parts',
57
+ 'udseende/dele': 'developer/parts',
58
+ 'udseende/dele/blokke': 'developer/parts/blocks',
59
+ 'udseende/dele/liste': 'developer/parts/list',
60
+ 'udseende/dele/vaerdier': 'developer/parts/values',
57
61
  'udseende/standardsider': 'appearance/default-pages',
58
62
  maaling: 'analytics',
59
63
  privatliv: 'privacy',
@@ -85,7 +89,7 @@ export const RENAMED = /** @type {Record<string, string>} */ ({
85
89
  'seo/interne-links': 'seo/internal-links',
86
90
  'seo/vurdering': 'seo/rating',
87
91
  blokvalg: 'block-settings',
88
- avanceret: 'advanced',
92
+ avanceret: 'developer/caching',
89
93
  licenser: 'licenses',
90
94
  konto: 'account',
91
95
  notifikationer: 'notifications',
@@ -31,9 +31,9 @@ test('the extra moves come first', () => {
31
31
  });
32
32
 
33
33
  test('only the old prefix of an address is renamed, the rest stays', () => {
34
- const renamed = { indstillinger: 'settings', 'udseende/dele': 'appearance/parts', liste: 'list' };
34
+ const renamed = { indstillinger: 'settings', 'udseende/dele': 'developer/parts', liste: 'list' };
35
35
  assert.equal(movedRoute('indstillinger/appstore', {}, renamed), 'settings/appstore');
36
- assert.equal(movedRoute('udseende/dele/liste/kontakt', {}, renamed), 'appearance/parts/liste/kontakt');
36
+ assert.equal(movedRoute('udseende/dele/liste/kontakt', {}, renamed), 'developer/parts/liste/kontakt');
37
37
  });
38
38
 
39
39
  test('a key segment * matches any one segment and keeps it', () => {
@@ -51,7 +51,7 @@ test('data inside an address is not renamed', () => {
51
51
  test('old prefixes are renamed, the data after them stays', () => {
52
52
  assert.equal(movedRoute('filer/filer/doc.pdf'), 'files/filer/doc.pdf');
53
53
  assert.equal(movedRoute('liste/temaer'), 'list/temaer');
54
- assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
54
+ assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'developer/parts/list/kontakt');
55
55
  assert.equal(movedRoute('indstillinger/appstore'), 'settings/appstore');
56
56
  assert.equal(movedRoute('udseende/farver'), 'appearance/colors');
57
57
  });
@@ -71,10 +71,13 @@ test('every old route prefix opens the screen where it is now', () => {
71
71
  'udseende/bevaegelse': 'appearance/motion',
72
72
  'udseende/sidekolonne': 'appearance/side-column',
73
73
  'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
74
- 'udseende/dele': 'appearance/parts',
75
- 'udseende/dele/editor': 'appearance/parts/editor',
76
- 'udseende/dele/blokke': 'appearance/parts/blocks',
77
- 'udseende/dele/vaerdier': 'appearance/parts/values',
74
+ 'udseende/dele': 'developer/parts',
75
+ 'udseende/dele/editor': 'developer/parts/editor',
76
+ 'udseende/dele/blokke': 'developer/parts/blocks',
77
+ 'udseende/dele/vaerdier': 'developer/parts/values',
78
+ 'appearance/parts': 'developer/parts',
79
+ 'appearance/parts/list/x': 'developer/parts/list/x',
80
+ advanced: 'developer/caching',
78
81
  'udseende/standardsider': 'appearance/default-pages',
79
82
  'udseende/logo': 'company/logo',
80
83
  maaling: 'analytics',
@@ -112,7 +115,7 @@ test('every old route prefix opens the screen where it is now', () => {
112
115
  'seo/interne-links': 'seo/internal-links',
113
116
  'seo/vurdering': 'seo/rating',
114
117
  blokvalg: 'block-settings',
115
- avanceret: 'advanced',
118
+ avanceret: 'developer/caching',
116
119
  licenser: 'licenses',
117
120
  konto: 'account',
118
121
  notifikationer: 'notifications',
@@ -139,12 +142,12 @@ test('the old colours route opens the colours screen under Appearance', () => {
139
142
  });
140
143
 
141
144
  test('an old parts route keeps a list id and opens at its English route', () => {
142
- assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
145
+ assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'developer/parts/list/kontakt');
143
146
  });
144
147
 
145
148
  test('the old theme routes open the screens under Appearance', () => {
146
149
  assert.equal(movedRoute('tema'), 'appearance');
147
- assert.equal(movedRoute('tema/dele/editor'), 'appearance/parts/editor');
150
+ assert.equal(movedRoute('tema/dele/editor'), 'developer/parts/editor');
148
151
  });
149
152
 
150
153
  test('the old booking and newsletter settings open the apps by their permanent ids', () => {
package/src/r2Caching.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Also here: planStatus, whether the account has Workers Paid (/api/plan, packages/editor/worker/plan-status.js), asked for "Betalt bygning".
2
2
  // R2 caching: the Worker's copy of the site's files in R2 (packages/editor/worker/files.js), turned on and off under "Indstillinger" ›
3
- // "Avanceret" (Advanced.tsx). It is the line of its bucket (`"r2_buckets": …`, the binding FILES) in the Worker's settings
3
+ // "Caching and builds" (Advanced.tsx). It is the line of its bucket (`"r2_buckets": …`, the binding FILES) in the Worker's settings
4
4
  // (config.wrangler): there when it is on, there as a comment ("// …") when it is off, so a deploy never asks for a bucket
5
5
  // Cloudflare does not have. Changed as text, so the file's comments stay. It can only be turned on when the Worker says
6
6
  // Cloudflare has the bucket (/api/r2, packages/editor/worker/r2-status.js). Where the bucket is kept ("Placering"): placed by Cloudflare,
package/src/registry.tsx CHANGED
@@ -12,7 +12,7 @@
12
12
  // blocks/, each with its own View.tsx and viewOf.ts for view.ts (it would differ from View.tsx only in case); they
13
13
  // are offered once the app is installed. A theme has no blocks of its own, only the look of the standard ones.
14
14
  import type { ReactNode } from 'react';
15
- import { appIdOf, installedApps } from './apps';
15
+ import { activeApps, appIdOf } from './apps';
16
16
  import { htmlBlock, type BlockData, type BlockDefinition, type BlockProps, type DrawBlock, type SiteBlock } from './blocks';
17
17
  import { check, templateProblems } from './check';
18
18
  import { slugOf, type Field, type Lead, type Template } from './schema';
@@ -82,7 +82,7 @@ export function offBlocks(files: { path: string; text: string }[]): string[] {
82
82
  /** The blocks not offered to put in, and that a new page does not start with: those turned off, and
83
83
  * those of the apps not installed (AppList.tsx). */
84
84
  export function notOffered(files: { path: string; text: string }[]): string[] {
85
- const installed = installedApps(files);
85
+ const installed = activeApps(files);
86
86
  return [...offBlocks(files), ...siteBlocks.filter((b) => b.app && !installed.includes(b.app)).map((b) => b.type)];
87
87
  }
88
88
 
package/src/schema.ts CHANGED
@@ -285,7 +285,7 @@ export type KindPart = {
285
285
  /** A list a standard block or an app brings ("Udtalelser", "Priser"), as the site's own, with its icon under "Lister". */
286
286
  export type OwnedList = FolderCollection & {
287
287
  icon: Icon;
288
- /** Made-up rows, as the files have them, for the parts under "Dele" and an app's pictures for the store (examples.ts). */
288
+ /** Made-up rows, as the files have them, for the parts under "Parts" and an app's pictures for the store (examples.ts). */
289
289
  examples?: Record<string, unknown>[];
290
290
  };
291
291
 
package/src/site.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // collection, a file, a part or a block as the site wrote it.
6
6
  import * as site from '@site/editor';
7
7
  import type { ColorPair } from '../lib/colors';
8
- import { appIdOf, installedApps, withKinds, type AppDefinition, type AppList, type SiteApp } from './apps';
8
+ import { activeApps, appIdOf, withKinds, type AppDefinition, type AppList, type SiteApp } from './apps';
9
9
  import { appBlocks, appFiles, appFolders, appPictures, appShared, appStores } from './appFiles';
10
10
  import type { BlockDefinition, SiteBlock } from './blocks';
11
11
  import type { ListRows } from './fields';
@@ -345,7 +345,7 @@ export const lists = [...site.lists, ...ownedLists.map((l) => l.id)];
345
345
  /** The lists under "Lister" now: the site's, the standard blocks' and those of the apps installed, without the cookie
346
346
  * list, which is a row under "Privatliv" (privacy.ts). */
347
347
  export const listsShown = (files: { path: string; text: string }[]) => {
348
- const installed = installedApps(files);
348
+ const installed = activeApps(files);
349
349
  return [...site.lists, ...[...standardLists, ...listsOf(writtenApps.filter((a) => installed.includes(a.id)))].map((l) => l.id)].filter(
350
350
  (id) => id !== site.config.lists.cookies,
351
351
  );
@@ -0,0 +1,24 @@
1
+ // The status of GitHub and Cloudflare, as the Worker says (/api/status, worker/service-status.js): what the editor's footer
2
+ // and its "System status" screen (ServiceStatus.tsx) show. Exposes serviceStatus. The types are in lib/serviceStatus.mjs.
3
+ // The last finished report is kept for a minute (a value, never a pending promise), so a navigation does not ask again.
4
+ import type { StatusReport } from '../lib/serviceStatus.mjs';
5
+
6
+ const KEEP_MS = 60_000;
7
+ // A slow Worker must not hold the dot or the status screen: after this the report is null (unknown).
8
+ const TIMEOUT_MS = 5000;
9
+ let kept: { report: StatusReport; at: number } | null = null;
10
+
11
+ /** The report, or null when it can't be fetched (no session, not JSON, offline, no answer in 5 seconds). A report less than a minute old is
12
+ * reused unless `fresh`; a failure or a timeout is not kept. */
13
+ export async function serviceStatus({ fresh = false }: { fresh?: boolean } = {}): Promise<StatusReport | null> {
14
+ if (!fresh && kept && Date.now() - kept.at < KEEP_MS) return kept.report;
15
+ try {
16
+ const res = await fetch('/api/status', { cache: 'no-store', signal: AbortSignal.timeout(TIMEOUT_MS) });
17
+ if (!res.ok || !res.headers.get('content-type')?.includes('application/json')) return null;
18
+ const report = (await res.json()) as StatusReport;
19
+ kept = { report, at: Date.now() };
20
+ return report;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
package/src/store.ts CHANGED
@@ -29,6 +29,7 @@ import type { StoreLicenses } from './licenseData';
29
29
  import { APP_API, THEME_API } from '../lib/contracts.mjs';
30
30
  import { RECORD, STANDARD, isText, validApp, validTheme } from '../lib/storeEntries.mjs';
31
31
  import { timedFetch } from './storeFetch';
32
+ import { appChanges as appChangesIn } from '../lib/appChanges.mjs';
32
33
  import { folderChanges } from '../lib/folderChanges.mjs';
33
34
 
34
35
  // The contracts this editor runs (lib/contracts.mjs, also read by the site's Worker).
@@ -425,20 +426,15 @@ export function themeChanges(
425
426
  }
426
427
 
427
428
  /** The changes that make an app's folder in the site (`current`, its files now) the store's version, with the note of
428
- * where it came from, or remove it (`next` null: uninstalling); and the site's file of apps with it among those
429
- * installed or not (`installed`: their ids now, `file`: the file). In one commit. */
429
+ * where it came from, or remove it (`next` null: uninstalling); and the site's file of apps (`file`) with it among those
430
+ * installed or not, an update keeping it turned off (lib/appChanges.mjs). In one commit. */
430
431
  export function appChanges(
431
432
  id: string,
432
433
  current: RepoFile[],
433
434
  next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
434
- installed: string[],
435
435
  file: RepoFile | undefined,
436
436
  ): Change[] {
437
- const changes = folderChanges(appFolder(id), current, next);
438
- const ids = [...new Set(installed.filter((x) => x !== id).concat(next ? [id] : []))].sort();
439
- const text = `${JSON.stringify({ installed: ids }, null, 2)}\n`;
440
- if (text !== file?.text) changes.push({ path: config.apps.installed, content: text, oid: file?.oid || null });
441
- return changes;
437
+ return appChangesIn(appFolder(id), config.apps.installed, id, current, next, file);
442
438
  }
443
439
 
444
440
  /** The note in an app's (or a theme's) folder of where it came from (store.json), from its files now (`current`). */