@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
@@ -0,0 +1,52 @@
1
+ // A titled list of versions with their release notes on the Morpheme page (#morpheme): per version a small heading, its
2
+ // cards (screenshot, title, text) in the editor's language (else English), then its other changes as a short list under
3
+ // "Also in this version"; for a version without notes, one row with its text. Exposes ReleaseNotes and VersionNotes.
4
+ // Depends on ReleaseNoteCard, ui.tsx, lib/releaseNotes.mjs (notesFor) and i18n.
5
+ import { ReleaseNoteCard } from './ReleaseNoteCard';
6
+ import { GroupTitle, Group, ListItem } from './ui';
7
+ import { locale, t } from './i18n';
8
+ import { notesFor } from '../lib/releaseNotes.mjs';
9
+ import type { ReleaseNote } from '../lib/releaseNotes.mjs';
10
+
11
+ export type VersionNotes = { version: string; text: string; notes?: Record<string, ReleaseNote[]> };
12
+
13
+ type Card = Extract<ReleaseNote, { kind: 'card' }>;
14
+
15
+ function VersionEntry({ entry }: { entry: VersionNotes }) {
16
+ const notes = notesFor(entry.notes, locale);
17
+ if (!notes?.length) {
18
+ return (
19
+ <Group>
20
+ <ListItem headline={t.cms.running(entry.version)} supporting={entry.text} />
21
+ </Group>
22
+ );
23
+ }
24
+ const cards = notes.filter((note): note is Card => note.kind === 'card');
25
+ const lines = notes.filter((note) => note.kind === 'line');
26
+ return (
27
+ <div className="mb-6 space-y-3">
28
+ <h3 className="px-4 text-sm font-medium text-muted">{t.cms.running(entry.version)}</h3>
29
+ {cards.map((note) => (
30
+ <ReleaseNoteCard key={note.id} note={note} />
31
+ ))}
32
+ {lines.length > 0 && (
33
+ <Group title={t.cms.alsoInThisVersion}>
34
+ {lines.map((note) => (
35
+ <ListItem key={note.id} headline={note.text} />
36
+ ))}
37
+ </Group>
38
+ )}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export function ReleaseNotes({ title, entries }: { title: string; entries: VersionNotes[] }) {
44
+ return (
45
+ <section className="mb-8">
46
+ <GroupTitle>{title}</GroupTitle>
47
+ {entries.map((entry) => (
48
+ <VersionEntry key={entry.version} entry={entry} />
49
+ ))}
50
+ </section>
51
+ );
52
+ }
package/src/Screen.tsx CHANGED
@@ -12,14 +12,17 @@
12
12
  // a form with its fields beside the text (`panel`). The column (editor.css .editor-column) stands centred on the window,
13
13
  // off-centre only as far as the panels beside it require.
14
14
  // Under the content of every screen is the editor's footer (EditorFooter.tsx), at the bottom of the window when the content
15
- // is short (the screen fills the column under the bar, TopBarLayout).
16
- import { useId, type MouseEvent, type ReactNode } from 'react';
15
+ // is short (the screen fills the column under the bar, TopBarLayout); under the footer is room for the floating action button
16
+ // only while the screen has one (fabPresence.tsx) and the content pushes the footer below the window (useFooterClearance.ts).
17
+ import { useId, useRef, type MouseEvent, type ReactNode } from 'react';
17
18
  import { createPortal } from 'react-dom';
18
19
  import { ArrowLeft, Bell, CaretRight, ArrowSquareOut } from '@phosphor-icons/react';
19
20
  import { t } from './i18n';
20
21
  import { IconButton, IconLink, MenuButton, TabPanel, TabRow, TopAppBar, useMenuLayout, type TabChoice } from './ui';
21
22
  import { up } from './nav';
22
23
  import EditorFooter from './EditorFooter';
24
+ import useFooterClearance from './useFooterClearance';
25
+ import { FabPresenceContext, useCountFabs } from './fabPresence';
23
26
  import { useMenuOpen } from './menuOpen';
24
27
  import { unseen, useNotifications } from './notificationData';
25
28
  import { openNotifications, useNotificationsOpen } from './notificationsOpen';
@@ -117,6 +120,9 @@ export default function Screen<T extends string>({
117
120
  else way();
118
121
  };
119
122
  const slot = useTopBarSlot();
123
+ const main = useRef<HTMLElement>(null);
124
+ const fabs = useCountFabs();
125
+ useFooterClearance(main, fabs.count > 0);
120
126
  // The way here, without the start screen: the logo in the bar is the way there.
121
127
  const crumbs = back && back.href !== '#' ? [back] : [];
122
128
  const bar = (
@@ -174,7 +180,8 @@ export default function Screen<T extends string>({
174
180
  {/* A row, so the column keeps its margins (editor.css .editor-column) and is as high as the rest of the window. */}
175
181
  <div className="flex flex-1">
176
182
  <main
177
- className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[calc(6rem+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 ${width}`}
183
+ ref={main}
184
+ className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[env(safe-area-inset-bottom)] data-[clearance=true]:pb-[calc(4.5rem+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 medium:pb-4 medium:data-[clearance=true]:pb-4 ${width}`}
178
185
  >
179
186
  {/* At expanded and wider: the way here, the title and the status, under the bar. */}
180
187
  <div className="mb-6 hidden min-w-0 items-center gap-x-2 expanded:flex">
@@ -196,15 +203,17 @@ export default function Screen<T extends string>({
196
203
  {pageActions && <div className="-my-3 ml-auto flex shrink-0 items-center gap-1">{pageActions}</div>}
197
204
  </div>
198
205
  {/* The content flows as blocks; the footer goes to the bottom of a short screen. */}
199
- <div className="min-w-0">
200
- {tabs ? (
201
- <TabPanel id={id} value={tabs.value}>
202
- {children}
203
- </TabPanel>
204
- ) : (
205
- children
206
- )}
207
- </div>
206
+ <FabPresenceContext.Provider value={fabs.report}>
207
+ <div className="min-w-0">
208
+ {tabs ? (
209
+ <TabPanel id={id} value={tabs.value}>
210
+ {children}
211
+ </TabPanel>
212
+ ) : (
213
+ children
214
+ )}
215
+ </div>
216
+ </FabPresenceContext.Provider>
208
217
  <EditorFooter />
209
218
  </main>
210
219
  </div>
@@ -1,8 +1,8 @@
1
1
  // How a page looks as a Google result, on a phone or a computer (tabs): the site's name and address, the title and the description.
2
- // Exposes SearchPreview. Depends on googleResult.mjs (the address line), TabRow and TabPanel in ui.tsx, shortDate (when.ts) and i18n.
2
+ // Exposes SearchPreview. Depends on googleResult.mjs (the address line, the bold keyword), TabRow and TabPanel in ui.tsx, shortDate (when.ts) and i18n.
3
3
  import { useId, useState } from 'react';
4
- import { DeviceMobile, Desktop } from '@phosphor-icons/react';
5
- import { searchCrumbs } from './googleResult.mjs';
4
+ import { DeviceMobile, Desktop, DotsThreeVertical } from '@phosphor-icons/react';
5
+ import { boldParts, searchCrumbs } from './googleResult.mjs';
6
6
  import { TabPanel, TabRow } from './ui';
7
7
  import { shortDate } from './when';
8
8
  import { t } from './i18n';
@@ -10,10 +10,12 @@ import { t } from './i18n';
10
10
  type Device = 'mobile' | 'desktop';
11
11
 
12
12
  /** How a page looks in Google's results, drawn as Google draws one, in Google's own colours (--google-* in editor.css,
13
- * light and dark with the editor): a circle with the site's first letter, the site's name over the address line, the title
14
- * in blue and the description, with a post's `date` before it. Tabs switch between a phone (opens first: the title cut off
15
- * after two lines, the description after three, on a card at most 360 px wide) and a computer (one line of title, two of
16
- * description, at most 600 px wide, Google's results column). `path`: the page's address on the site. */
13
+ * light and dark with the editor): a ringed circle with the site's first letter, the site's name over the address line
14
+ * (https:// first) and a menu icon, the title in blue and the description, with a post's `date` before it and the
15
+ * `keyword` in bold in the description. Tabs switch between a phone (opens first: the title cut off after two lines, the
16
+ * description after three, at most 360 px wide between two 8 px bands, the menu icon at the right edge) and a computer
17
+ * (one line of title, two of description, at most 600 px wide, Google's results column, the menu icon after the
18
+ * address). `path`: the page's address on the site. `keyword`: the page's primary keyword, if it has one. */
17
19
  export function SearchPreview({
18
20
  domain,
19
21
  path,
@@ -21,6 +23,7 @@ export function SearchPreview({
21
23
  title,
22
24
  description,
23
25
  date,
26
+ keyword,
24
27
  }: {
25
28
  domain: string;
26
29
  path: string;
@@ -28,10 +31,12 @@ export function SearchPreview({
28
31
  title: string;
29
32
  description: string;
30
33
  date?: string;
34
+ keyword?: string;
31
35
  }) {
32
36
  const id = useId();
33
37
  const [device, setDevice] = useState<Device>('mobile');
34
38
  const phone = device === 'mobile';
39
+ const menu = <DotsThreeVertical aria-hidden="true" className="shrink-0 text-(--google-text)" />;
35
40
  return (
36
41
  <div className="space-y-3">
37
42
  <TabRow
@@ -47,26 +52,44 @@ export function SearchPreview({
47
52
  <TabPanel id={id} value={device}>
48
53
  <figure
49
54
  aria-label={t.forms.searchPreview.result}
50
- className={`space-y-1 border border-line bg-(--google-background) p-4 font-[arial,sans-serif] ${
51
- phone ? 'max-w-[360px] rounded-3xl' : 'max-w-[600px] rounded-lg'
55
+ className={`bg-(--google-background) p-4 font-[arial,sans-serif] ${
56
+ phone ? 'max-w-[360px] border-y-8 border-(--google-separator)' : 'max-w-[600px]'
52
57
  }`}
53
58
  >
54
59
  <div className="flex items-center gap-3">
55
60
  <span
56
61
  aria-hidden="true"
57
- className="flex size-[26px] shrink-0 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--google-text)_15%,transparent)] text-xs text-(--google-site)"
62
+ className="flex size-[28px] shrink-0 items-center justify-center rounded-full border border-(--google-icon-ring) bg-(--google-icon-fill) text-xs text-(--google-icon-letter)"
58
63
  >
59
64
  {[...siteName][0]?.toLocaleUpperCase()}
60
65
  </span>
61
66
  <span className="min-w-0">
62
67
  <span className="block truncate text-sm text-(--google-site)">{siteName}</span>
63
- <span className="block truncate text-xs text-(--google-text)">{searchCrumbs(domain, path)}</span>
68
+ <span className="flex items-center gap-1">
69
+ <span className="truncate text-xs text-(--google-text)">{searchCrumbs(domain, path)}</span>
70
+ {!phone && menu}
71
+ </span>
64
72
  </span>
73
+ {phone && <span className="ml-auto">{menu}</span>}
65
74
  </div>
66
- <span className={`block text-(--google-title) ${phone ? 'line-clamp-2 text-lg' : 'truncate text-xl'}`}>{title}</span>
67
- <p className={`text-sm leading-[22px] text-(--google-text) ${phone ? 'line-clamp-3' : 'line-clamp-2'}`}>
75
+ <span
76
+ className={`block text-[20px] leading-[26px] ${
77
+ phone ? 'mt-3 line-clamp-2 text-(--google-title-phone)' : 'mt-1.5 truncate text-(--google-title)'
78
+ }`}
79
+ >
80
+ {title}
81
+ </span>
82
+ <p className={`mt-1 text-sm leading-[22px] text-(--google-text) ${phone ? 'line-clamp-3' : 'line-clamp-2'}`}>
68
83
  {date && <span className="text-(--google-date)">{shortDate(date)} — </span>}
69
- {description}
84
+ {boldParts(description, keyword).map((part, i) =>
85
+ part.bold ? (
86
+ <b key={i} className="font-bold">
87
+ {part.text}
88
+ </b>
89
+ ) : (
90
+ part.text
91
+ ),
92
+ )}
70
93
  </p>
71
94
  </figure>
72
95
  </TabPanel>
@@ -1,4 +1,4 @@
1
- // "Dele" › "UI-komponenter" (BlockParts.tsx), the group "Kontakter, valg og sideark": the theme's switches, setting rows, radio buttons,
1
+ // "Parts" › "UI-komponenter" (BlockParts.tsx), the group "Kontakter, valg og sideark": the theme's switches, setting rows, radio buttons,
2
2
  // sliders, dividers and side sheets (packages/editor/theme/ui/, through @site/editor) as the site draws them, in every
3
3
  // kind and state (the sliders also centered, in each size, with an icon and vertical), with made-up texts. The
4
4
  // switches, radio buttons and sliders can be tried (a slider's tracks and labels follow it here as on the site);
@@ -0,0 +1,59 @@
1
+ // "System status" (#status), opened from the editor's footer: the services a site needs (GitHub, Cloudflare), a group
2
+ // each with its components and their status, and a link to the provider's own status page (/api/status, statusReport.ts).
3
+ // Exposes STATUS_ROUTE and the screen. Uses Screen, Group, ListItem and EmptyNote from ui.tsx and the theme's Badge.
4
+ import { useEffect, useState } from 'react';
5
+ import { ArrowSquareOut } from '@phosphor-icons/react';
6
+ import Screen from './Screen';
7
+ import { EmptyNote, Group, ListItem } from './ui';
8
+ import { SiteBadge } from './themeUi';
9
+ import { backFrom } from './site';
10
+ import { t } from './i18n';
11
+ import { serviceStatus } from './statusReport';
12
+ import type { StatusReport } from '../lib/serviceStatus.mjs';
13
+
14
+ export const STATUS_ROUTE = 'status';
15
+
16
+ export default function ServiceStatus() {
17
+ // undefined while it loads, null when it can't be fetched.
18
+ const [report, setReport] = useState<StatusReport | null>();
19
+ useEffect(() => {
20
+ let on = true;
21
+ serviceStatus({ fresh: true }).then((x) => on && setReport(x));
22
+ return () => {
23
+ on = false;
24
+ };
25
+ }, []);
26
+
27
+ return (
28
+ <Screen title={t.footer.status} wide back={backFrom(STATUS_ROUTE)}>
29
+ {report === undefined ? (
30
+ <EmptyNote busy>{t.footer.loading}</EmptyNote>
31
+ ) : report === null ? (
32
+ <EmptyNote>{t.footer.state.unknown}</EmptyNote>
33
+ ) : (
34
+ report.providers.map((provider) => (
35
+ <Group key={provider.id} title={provider.name}>
36
+ {provider.components.map((component) => {
37
+ const status = t.footer.component[component.status];
38
+ return (
39
+ <ListItem
40
+ key={component.id}
41
+ headline={component.name}
42
+ supporting={status}
43
+ trailing={<SiteBadge tone={component.state} label={status} />}
44
+ />
45
+ );
46
+ })}
47
+ <ListItem
48
+ headline={t.footer.openStatusPage}
49
+ href={provider.page}
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ arrow={ArrowSquareOut}
53
+ />
54
+ </Group>
55
+ ))
56
+ )}
57
+ </Screen>
58
+ );
59
+ }
@@ -10,7 +10,7 @@ import { ChoiceRow, EmptyNote, Group } from './ui';
10
10
  import type { Env } from './App';
11
11
  import { kindAndName } from './deploy';
12
12
  import { ask } from './ask';
13
- import { installedApps } from './apps';
13
+ import { activeApps } from './apps';
14
14
  import { siteBlocks } from './registry';
15
15
  import { locale, siteT, t } from './i18n';
16
16
  import { sideChoices } from '../lib/side-block.mjs';
@@ -38,7 +38,7 @@ export function sideSettingOf(files: { path: string; text: string }[]): string |
38
38
 
39
39
  /** The installed blocks that can be drawn beside a page's text, with their names in the editor's language. */
40
40
  export const sideChoicesOf = (files: { path: string; text: string }[]) =>
41
- sideChoices(siteBlocks, sources, installedApps(files)) as { type: string; label: string }[];
41
+ sideChoices(siteBlocks, sources, activeApps(files)) as { type: string; label: string }[];
42
42
 
43
43
  /** "Sidekolonne": "Ingen" and each block that can be drawn beside the text, the one chosen ticked. A tap on another asks
44
44
  * first and then saves it (`{ "block": "<type>" }`, or `{ "block": null }`) at once, in its own commit, as an icon pack is. */
@@ -1,4 +1,4 @@
1
- // The box in "Appstore" for a site without a key to the store: the button "Connect to Morpheme", or a note for editors who may only read.
1
+ // The box on the Morpheme page (#morpheme) and above the store lists for a site without a key to the store: the button "Connect to Morpheme", or a note for editors who may only read.
2
2
  // Exposes StoreConnectNotice({ mayChange, onConnected }); uses Notice, Button, EmptyNote (ui.tsx), snackbar (lib/snackbar.ts) and connect (storeConnection.ts).
3
3
  import { useState } from 'react';
4
4
  import { snackbar } from '../lib/snackbar';
@@ -1,10 +1,11 @@
1
- // The row at the bottom of "Appstore" that says the site is connected to Morpheme, with the button "Disconnect from Morpheme" (asks first).
2
- // Exposes StoreConnectedRow({ site, onDisconnected }); uses Group, ListItem, Button (ui.tsx), ask (ask.tsx), snackbar (lib/snackbar.ts) and disconnect (storeConnection.ts).
1
+ // The row on the Morpheme page (#morpheme) that says the site is connected to Morpheme, with the Morpheme logo, and under it the button "Disconnect from Morpheme" (asks first).
2
+ // Exposes StoreConnectedRow({ site, onDisconnected }); uses ListItem, LeadingIcon, Button, plainListClass (ui.tsx), MorphemeLogo, ask (ask.tsx), snackbar (lib/snackbar.ts) and disconnect (storeConnection.ts).
3
3
  import { useState } from 'react';
4
4
  import { snackbar } from '../lib/snackbar';
5
5
  import { ask } from './ask';
6
6
  import { disconnect } from './storeConnection';
7
- import { Button, Group, ListItem } from './ui';
7
+ import { Button, LeadingIcon, ListItem, plainListClass } from './ui';
8
+ import { MorphemeLogo } from './MorphemeLogo';
8
9
  import { t } from './i18n';
9
10
 
10
11
  export function StoreConnectedRow({ site, onDisconnected }: { site: string; onDisconnected: () => void }) {
@@ -21,16 +22,21 @@ export function StoreConnectedRow({ site, onDisconnected }: { site: string; onDi
21
22
  }
22
23
  }
23
24
  return (
24
- <Group>
25
- <ListItem
26
- headline={t.apps.connected}
27
- supporting={site}
28
- actions={
29
- <Button disabled={busy} onClick={disconnectNow}>
30
- {t.apps.disconnect}
31
- </Button>
32
- }
33
- />
34
- </Group>
25
+ <>
26
+ <ul className={plainListClass}>
27
+ <ListItem
28
+ leading={
29
+ <LeadingIcon>
30
+ <MorphemeLogo />
31
+ </LeadingIcon>
32
+ }
33
+ headline={t.apps.connected}
34
+ supporting={site}
35
+ />
36
+ </ul>
37
+ <Button kind="text" size="small" disabled={busy} onClick={disconnectNow}>
38
+ {t.apps.disconnect}
39
+ </Button>
40
+ </>
35
41
  );
36
42
  }
@@ -1,10 +1,9 @@
1
- // What a store screen (apps or themes) shows around its list: StoreRound (draws it again after connecting), StoreNotice (connect, loading or failed note) and StoreFooter (the connected row).
2
- // Exposes StoreRound, useStoreKey, useStoreConnected, StoreNotice, StoreFooter; uses StoreConnectNotice, StoreConnectedRow, storeConnection.ts and env.store.connected.
1
+ // What a store screen (apps or themes) shows around its list: StoreRound (draws it again after connecting), StoreNotice (connect, loading or failed note).
2
+ // Exposes StoreRound, useStoreKey, useStoreConnected, StoreNotice; uses StoreConnectNotice, storeConnection.ts and env.store.connected.
3
3
  import { Fragment, useEffect, useState, type ReactNode } from 'react';
4
4
  import { EmptyNote } from './ui';
5
5
  import type { Env } from './App';
6
6
  import { StoreConnectNotice } from './StoreConnectNotice';
7
- import { StoreConnectedRow } from './StoreConnectedRow';
8
7
  import { connection, type StoreConnection } from './storeConnection';
9
8
  import { t } from './i18n';
10
9
 
@@ -58,10 +57,3 @@ export function StoreNotice({
58
57
  if (read === 'failed') return <EmptyNote>{t.apps.storeFailed}</EmptyNote>;
59
58
  return null;
60
59
  }
61
-
62
- /** The row at the end of a store list that says the site is connected, with its button to disconnect. */
63
- export function StoreFooter({ keyInfo, again }: { keyInfo: StoreConnection | null; again: () => void }) {
64
- return keyInfo?.connected && keyInfo.by === 'button' && keyInfo.mayChange ? (
65
- <StoreConnectedRow site={keyInfo.site} onDisconnected={again} />
66
- ) : null;
67
- }
@@ -3,7 +3,7 @@
3
3
  import Screen from './Screen';
4
4
  import { Group, useKept } from './ui';
5
5
  import type { Env } from './App';
6
- import { StoreFooter, StoreNotice, StoreRound, useStoreConnected, useStoreKey } from './StoreStatus';
6
+ import { StoreNotice, StoreRound, useStoreConnected, useStoreKey } from './StoreStatus';
7
7
  import { StoreSearchRow, useStateFilter } from './StoreSearchRow';
8
8
  import { useSort } from './storeSort';
9
9
  import { byText } from './schema';
@@ -56,7 +56,6 @@ function ThemeStoreList({ env, again }: { env: Env; again: () => void }) {
56
56
  </Group>
57
57
  ))
58
58
  )}
59
- <StoreFooter keyInfo={key} again={again} />
60
59
  </Screen>
61
60
  );
62
61
  }
package/src/Tokens.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Dele" › "Værdier" (#appearance/parts/values): the theme's design tokens, as the theme in use sets them, to see them in one
1
+ // "Parts" › "Værdier" (#developer/parts/values): the theme's design tokens, as the theme in use sets them, to see them in one
2
2
  // place: the corners, one row for each size the theme uses (the theme's base, base.css and components.css, which every
3
3
  // theme has, and its UI parts', ui.css: each part's own variable, else the one ui.css gives it instead), named by
4
4
  // Material 3's corner radius scale where the size is one of its steps, with what has it under; the shadows, one row for
@@ -15,7 +15,7 @@ import { Group, LeadingToken, ListItem, type TokenKind } from './ui';
15
15
  import { locale, t } from './i18n';
16
16
 
17
17
  const say = t.parts.tokens;
18
- const back = { href: '#appearance/parts', label: t.common.parts.editorParts };
18
+ const back = { href: '#developer/parts', label: t.common.parts.editorParts };
19
19
 
20
20
  /** A token: its CSS variable, its name here, and for a part's own the token or the value ui.css gives it when the theme
21
21
  * does not set it (its `var(--chip-radius, var(--block-control-radius))`). */
package/src/apps.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  // worker.js, build.mjs its part of the site's Worker (/api/<app>/…, each hour) and a step after the site is built
33
33
  // (packages/editor/lib/apps.mjs);
34
34
  // csp.json what it loads from elsewhere on the site (packages/editor/lib/csp.mjs).
35
- // An app that is not installed is not offered in the editor: its blocks are not under "Indsæt" nor
35
+ // An app that is not installed, or turned off ("off" in the file of apps), is not offered in the editor: its blocks are not under "Indsæt" nor
36
36
  // "Blokindstillinger", its kinds not under "+", its lists not under "Lister", its settings not on its page. What was made with it stays,
37
37
  // on the pages and on the site, and can still be edited. Installed, its blocks are under its name in "Blokindstillinger". The
38
38
  // apps installed are in the site's file of apps (config.apps.installed), each app's settings in a file of
@@ -46,13 +46,14 @@ import type { ConsentCategory } from '../lib/cookieCategories.mjs';
46
46
  import type { AppCategory, AppCookie, AppNeeds, AppShot, StoreRecord } from './store';
47
47
  import type { ThirdParty } from './licenseData';
48
48
  import { config } from '@site/config';
49
+ import { activeOf, readAppsFile } from '../lib/appsFile.mjs';
49
50
 
50
51
  /** An app's settings, on its page once it is installed (#apps/<app>, AppList.tsx). */
51
52
  export type AppSettings = {
52
53
  fields: Field[];
53
54
  /** The settings before any are saved, in the site's language. */
54
55
  values: Record<string, unknown>;
55
- /** Made-up settings over `values`, which its blocks and kinds are drawn with under "Dele" (exampleData in
56
+ /** Made-up settings over `values`, which its blocks and kinds are drawn with under "Parts" (exampleData in
56
57
  * examples.ts), where the site's own are never shown (MobilePay's number). */
57
58
  example?: Record<string, unknown>;
58
59
  /** The value a save of these settings sets, said in the notice after it ("CMS-version set to 0.10.0"), asked with the
@@ -82,7 +83,7 @@ export type AppKind = {
82
83
  opens?: Opens;
83
84
  };
84
85
 
85
- /** How an entry of an app's kind looks on the site, beside its form and under "Dele": its own drawing, or its part in the
86
+ /** How an entry of an app's kind looks on the site, beside its form and under "Parts": its own drawing, or its part in the
86
87
  * list's own drawing (`collection`: the list it is a kind of). */
87
88
  export function kindDraw(kind: AppKind, collection?: { drawPart?: FolderCollection['drawPart'] }): AppKind['draw'] {
88
89
  if (kind.draw) return kind.draw;
@@ -147,8 +148,9 @@ export type AppDefinition = {
147
148
  kinds?: Record<string, AppKind>;
148
149
  /** The lists it brings ("Priser"), under "Lister" while it is installed. */
149
150
  lists?: AppList[];
150
- /** In the app store (store.ts): its version, X.Y.Z ("1.2.1"): X for a major release, Y for new features, Z for fixes
151
- * or fitting a theme, the numbers after the one raised back to 0; without it, it is the site's own. X is raised only
151
+ /** Its version, shown on its page: in the app store (store.ts) the one released, and a site-local app can set one too.
152
+ * X.Y.Z ("1.2.1"): X for a major release, Y for new features, Z for fixes
153
+ * or fitting a theme, the numbers after the one raised back to 0; without one, the page shows none. X is raised only
152
154
  * when the app itself changes something a site that has it can break on (a block, field or setting gone or changed),
153
155
  * never for a new contract on its own: which editor it runs in is its `api` (APP_API), and fitting it to a new one is
154
156
  * Z (Y with new features), since a major release is not updated to on its own. */
@@ -257,15 +259,15 @@ export { appIdOf } from '../lib/appId.mjs';
257
259
  /** The file of an app's settings (config.apps.settings/<id>.json). */
258
260
  export const settingsFile = (id: string) => `${config.apps.settings}/${id}.json`;
259
261
 
260
- /** The apps installed, by their ids, from the site's file of apps. */
261
- export function installedApps(files: { path: string; text: string }[]): string[] {
262
- try {
263
- const installed = JSON.parse(files.find((f) => f.path === config.apps.installed)?.text ?? '{}').installed;
264
- return Array.isArray(installed) ? installed.map(String) : [];
265
- } catch {
266
- return [];
267
- }
268
- }
262
+ /** The site's file of apps as the files have it now: the apps installed and those turned off. */
263
+ export const appsState = (files: { path: string; text: string }[]) =>
264
+ readAppsFile(files.find((f) => f.path === config.apps.installed)?.text);
265
+
266
+ /** The apps installed, by their ids, from the site's file of apps, also those turned off. */
267
+ export const installedApps = (files: { path: string; text: string }[]): string[] => appsState(files).installed;
268
+
269
+ /** The apps installed and not turned off, by their ids: those that run, offer their blocks and show their screens. */
270
+ export const activeApps = (files: { path: string; text: string }[]): string[] => activeOf(appsState(files));
269
271
 
270
272
  /** Each app's settings as the files have them now, by its id, over those it starts with (`values`). */
271
273
  export function appSettings(
@@ -1,4 +1,4 @@
1
- // The build settings the editor writes under "Indstillinger" › "Avanceret" (Advanced.tsx): src/data/build.json, read by
1
+ // The build settings the editor writes under Settings › Developer › Caching and builds (Advanced.tsx): src/data/build.json, read by
2
2
  // the build (fast content lane) and by the editor's wait for a build. Exposes buildSettingsOf and withBuildSetting; the
3
3
  // defaults come from packages/editor/lib/build-settings.mjs.
4
4
  import { readBuildSettings } from '../lib/build-settings.mjs';
package/src/cmsView.d.mts CHANGED
@@ -3,6 +3,7 @@ export function cmsView(status: any, now?: number): {
3
3
  checkedAt: any;
4
4
  latest: any;
5
5
  notes: any[];
6
+ installed: any;
6
7
  action: string | null;
7
8
  npmCommand: string | null;
8
9
  update: {
package/src/cmsView.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // Transforms CMS status into a view for the update page.
2
- // Exposes cmsView and DONE_SHOWN.
2
+ // Exposes cmsView and DONE_SHOWN. `installed` passes the notes of the update just installed on as the Worker gives them.
3
3
  // Depends on cms-version for version comparison.
4
4
 
5
5
  import { compareVersions } from '../lib/cms-version.mjs';
@@ -77,5 +77,7 @@ export function cmsView(status, now = Date.now()) {
77
77
  problems.push('slow-undo');
78
78
  }
79
79
 
80
- return { running, checkedAt, latest, notes, action, npmCommand, update, problems };
80
+ const installed = Array.isArray(status.installed) ? status.installed : [];
81
+
82
+ return { running, checkedAt, latest, notes, installed, action, npmCommand, update, problems };
81
83
  }
@@ -150,3 +150,9 @@ test('pre-release running', () => {
150
150
  const view = cmsView(status, now);
151
151
  assert.equal(view.latest, null);
152
152
  });
153
+
154
+ test('passes the installed notes on, or an empty list', () => {
155
+ const installed = [{ version: '0.1.1', text: 'Fixes', notes: { 'en-US': [] } }];
156
+ assert.deepEqual(cmsView(makeStatus({ installed }), now).installed, installed);
157
+ assert.deepEqual(cmsView(makeStatus({}), now).installed, []);
158
+ });
package/src/editor.css CHANGED
@@ -21,13 +21,20 @@
21
21
  --editor-status-bad-text: #cc0000;
22
22
  --editor-status-bad-surface: color-mix(in srgb, #ff4e42 10%, var(--surface));
23
23
 
24
- /* A Google result (SearchPreview.tsx) in Google's own colours, light: its background, the title, the site's name, the
25
- address and the text, and the date before the text. */
24
+ /* A Google result (SearchPreview.tsx) in Google's own colours, light: its background, the title (on a computer, and on a
25
+ phone), the site's name, the address and the text, the date before the text, and the 8 px band above and below a
26
+ phone's result. */
26
27
  --google-background: #fff;
27
28
  --google-title: #1a0dab;
29
+ --google-title-phone: #1558d6;
28
30
  --google-site: #202124;
29
31
  --google-text: #4d5156;
30
32
  --google-date: #70757a;
33
+ --google-separator: #ecedef;
34
+ /* The site's circle: its fill and ring and the letter in it, the same in light and dark. */
35
+ --google-icon-fill: #fff;
36
+ --google-icon-ring: #dadce0;
37
+ --google-icon-letter: #202124;
31
38
 
32
39
  /* The dotted frame round an H2 section of a page or blog post (sectionFrame.ts): its line, the space between the line and
33
40
  the text, the space above a section, and the space between two parts inside it (em: it scales with the part's text). */
@@ -52,9 +59,11 @@
52
59
  :where(:root:not([data-theme='light'])) {
53
60
  --google-background: #1f1f1f;
54
61
  --google-title: #99c3ff;
62
+ --google-title-phone: #99c3ff;
55
63
  --google-site: #dadce0;
56
64
  --google-text: #bfbfbf;
57
65
  --google-date: #9aa0a6;
66
+ --google-separator: #303134;
58
67
  --editor-status-good-text: var(--editor-status-good);
59
68
  --editor-status-average-text: var(--editor-status-average);
60
69
  --editor-status-bad-text: var(--editor-status-bad);
@@ -63,9 +72,11 @@
63
72
  :where(:root[data-theme='dark']) {
64
73
  --google-background: #1f1f1f;
65
74
  --google-title: #99c3ff;
75
+ --google-title-phone: #99c3ff;
66
76
  --google-site: #dadce0;
67
77
  --google-text: #bfbfbf;
68
78
  --google-date: #9aa0a6;
79
+ --google-separator: #303134;
69
80
  --editor-status-good-text: var(--editor-status-good);
70
81
  --editor-status-average-text: var(--editor-status-average);
71
82
  --editor-status-bad-text: var(--editor-status-bad);
package/src/examples.ts CHANGED
@@ -1,4 +1,4 @@
1
- // What "Dele" (#appearance/parts: Parts.tsx, BlockParts.tsx) shows the parts and blocks with: made-up pages, posts, tags,
1
+ // What "Parts" (#developer/parts: Parts.tsx, BlockParts.tsx) shows the parts and blocks with: made-up pages, posts, tags,
2
2
  // testimonials, the lists' made-up rows (`examples` of a list), cookies and contact details in the editor's language,
3
3
  // the apps' made-up settings (AppSettings.example), and a plain picture, never what the site has, so the parts look the
4
4
  // same on every site.
@@ -76,7 +76,7 @@ export function exampleData(): BlockData {
76
76
  }
77
77
 
78
78
  /** An entry of a kind of a list of kinds (a pop-up, Mailchimp's) as an example: a new one of the kind, with the
79
- * defaults of the fields it has, the made-up name, heading and text, and the plain picture. "Dele" draws each kind
79
+ * defaults of the fields it has, the made-up name, heading and text, and the plain picture. "Parts" draws each kind
80
80
  * with it, and "Blokindstillinger" an app's kind no entry is made with. */
81
81
  export function kindExample(list: FolderCollection, kind: string): Record<string, unknown> {
82
82
  const name = list.kinds!;
@@ -0,0 +1,24 @@
1
+ // Lets a screen know it shows a FAB: Screen (Screen.tsx) provides the context, FabMenu (ui.tsx) calls useRegisterFab while
2
+ // it is drawn. Exposes FabPresenceContext, useCountFabs and useRegisterFab. Used to leave room under the footer for the FAB
3
+ // only (useFooterClearance.ts).
4
+ import { createContext, useCallback, useContext, useEffect, useState } from 'react';
5
+
6
+ /** Called with +1 when a FAB appears on the screen and -1 when it goes. Absent outside a Screen. */
7
+ export const FabPresenceContext = createContext<((change: 1 | -1) => void) | null>(null);
8
+
9
+ /** For a Screen: how many FABs its content shows, and the value for FabPresenceContext. */
10
+ export function useCountFabs() {
11
+ const [count, setCount] = useState(0);
12
+ const report = useCallback((change: 1 | -1) => setCount((n) => n + change), []);
13
+ return { count, report };
14
+ }
15
+
16
+ /** For a FAB: tells its Screen it is there while `active`. */
17
+ export function useRegisterFab(active: boolean) {
18
+ const report = useContext(FabPresenceContext);
19
+ useEffect(() => {
20
+ if (!active || !report) return;
21
+ report(1);
22
+ return () => report(-1);
23
+ }, [active, report]);
24
+ }