@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.
- package/CHANGELOG.md +18 -0
- package/defaults/editor.ts +3 -14
- package/icons/lucide/dots-three-vertical.svg +1 -0
- package/icons/material-symbols/dots-three-vertical.svg +1 -0
- package/icons/phosphor/dots-three-vertical.svg +1 -0
- package/lib/appChanges.d.mts +16 -0
- package/lib/appChanges.mjs +20 -0
- package/lib/appChanges.test.mjs +30 -0
- package/lib/apps.d.mts +2 -2
- package/lib/apps.mjs +13 -7
- package/lib/appsFile.d.mts +20 -0
- package/lib/appsFile.mjs +57 -0
- package/lib/appsFile.test.mjs +53 -0
- package/lib/build-settings.mjs +1 -1
- package/lib/changelog.d.mts +7 -0
- package/lib/changelog.mjs +16 -5
- package/lib/footerClearance.d.mts +1 -0
- package/lib/footerClearance.mjs +9 -0
- package/lib/footerClearance.test.mjs +24 -0
- package/lib/footerLinks.d.mts +12 -0
- package/lib/footerLinks.mjs +26 -0
- package/lib/footerLinks.test.mjs +32 -0
- package/lib/google-result.test.mjs +40 -9
- package/lib/menu.mjs +1 -1
- package/lib/menu.test.mjs +9 -8
- package/lib/releaseNotes.d.mts +136 -0
- package/lib/releaseNotes.mjs +249 -0
- package/lib/releaseNotes.test.mjs +267 -0
- package/lib/serviceFeed.d.mts +2 -0
- package/lib/serviceFeed.mjs +13 -0
- package/lib/serviceFeed.test.mjs +53 -0
- package/lib/serviceStatus.d.mts +72 -0
- package/lib/serviceStatus.mjs +143 -0
- package/lib/serviceStatus.test.mjs +136 -0
- package/lib/strayDividers.d.mts +6 -0
- package/lib/strayDividers.mjs +15 -0
- package/lib/strayDividers.test.mjs +25 -0
- package/package.json +5 -3
- package/release-notes/i18n/da-DK/pending/apps-turn-off.json +6 -0
- package/release-notes/i18n/da-DK/pending/apps-turn-off.webp +0 -0
- package/release-notes/i18n/da-DK/pending/developer-menu.json +5 -0
- package/release-notes/i18n/da-DK/pending/dynamic-lists-page.json +5 -0
- package/release-notes/i18n/da-DK/pending/editor-footer.json +6 -0
- package/release-notes/i18n/da-DK/pending/editor-footer.webp +0 -0
- package/release-notes/i18n/da-DK/pending/editor-toolbar-dock.json +5 -0
- package/release-notes/i18n/da-DK/pending/google-preview-closer.json +6 -0
- package/release-notes/i18n/da-DK/pending/google-preview-closer.webp +0 -0
- package/release-notes/i18n/da-DK/pending/morpheme-connection-page.json +5 -0
- package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.json +6 -0
- package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.webp +0 -0
- package/release-notes/i18n/en-US/pending/apps-turn-off.json +6 -0
- package/release-notes/i18n/en-US/pending/apps-turn-off.webp +0 -0
- package/release-notes/i18n/en-US/pending/developer-menu.json +5 -0
- package/release-notes/i18n/en-US/pending/dynamic-lists-page.json +5 -0
- package/release-notes/i18n/en-US/pending/editor-footer.json +6 -0
- package/release-notes/i18n/en-US/pending/editor-footer.webp +0 -0
- package/release-notes/i18n/en-US/pending/editor-toolbar-dock.json +5 -0
- package/release-notes/i18n/en-US/pending/google-preview-closer.json +6 -0
- package/release-notes/i18n/en-US/pending/google-preview-closer.webp +0 -0
- package/release-notes/i18n/en-US/pending/morpheme-connection-page.json +5 -0
- package/release-notes/i18n/en-US/pending/quiet-lamps-wander.json +6 -0
- package/release-notes/i18n/en-US/pending/quiet-lamps-wander.webp +0 -0
- package/scripts/app-screenshots.mjs +4 -4
- package/scripts/build-settings-problems.mjs +1 -1
- package/scripts/built-site.mjs +1 -1
- package/scripts/check-content.mjs +1 -1
- package/scripts/check-release-notes.mjs +12 -0
- package/scripts/check-worker.mjs +1 -1
- package/scripts/editor-screens.mjs +32 -16
- package/scripts/release-notes-pending.mjs +46 -0
- package/scripts/release-notes.mjs +39 -0
- package/site/apps.ts +10 -4
- package/site/consent.ts +3 -3
- package/site/defineSite.mjs +1 -1
- package/site/icons.d.mts +4 -0
- package/site/icons.mjs +2 -0
- package/site/pageColors.ts +3 -3
- package/site/siteCsp.d.mts +1 -1
- package/site/siteCsp.mjs +3 -2
- package/src/ActionParts.tsx +1 -1
- package/src/Advanced.tsx +4 -4
- package/src/App.tsx +22 -17
- package/src/AppEnabledSwitch.tsx +21 -0
- package/src/AppList.tsx +47 -19
- package/src/AppSettingsTab.tsx +9 -5
- package/src/BlockParts.tsx +7 -7
- package/src/BlockSettings.tsx +3 -3
- package/src/CmsPage.tsx +11 -10
- package/src/Colors.tsx +2 -2
- package/src/CommunicationParts.tsx +18 -2
- package/src/ContainmentParts.tsx +1 -1
- package/src/DataForm.tsx +1 -0
- package/src/EditorFooter.tsx +19 -9
- package/src/EntryForm.tsx +1 -0
- package/src/FooterCopyright.tsx +7 -0
- package/src/FooterDivider.tsx +5 -0
- package/src/FooterLink.tsx +18 -0
- package/src/FooterLinks.tsx +34 -0
- package/src/Library.tsx +21 -10
- package/src/MainDrawerParts.tsx +1 -1
- package/src/MarkdownEditor.tsx +1 -1
- package/src/ModalSheetSample.tsx +1 -1
- package/src/MorphemeConnection.tsx +30 -0
- package/src/MorphemeLogo.tsx +9 -0
- package/src/NavigationParts.tsx +1 -1
- package/src/NavigationRestParts.tsx +1 -1
- package/src/Parts.tsx +9 -9
- package/src/PartsShown.tsx +1 -1
- package/src/PickerParts.tsx +1 -1
- package/src/ReleaseNoteCard.tsx +17 -0
- package/src/ReleaseNotes.tsx +52 -0
- package/src/Screen.tsx +21 -12
- package/src/SearchPreview.tsx +37 -14
- package/src/SelectionParts.tsx +1 -1
- package/src/ServiceStatus.tsx +59 -0
- package/src/SideColumn.tsx +2 -2
- package/src/StoreConnectNotice.tsx +1 -1
- package/src/StoreConnectedRow.tsx +20 -14
- package/src/StoreStatus.tsx +2 -10
- package/src/ThemeStoreScreen.tsx +1 -2
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +16 -14
- package/src/buildSettings.ts +1 -1
- package/src/cmsView.d.mts +1 -0
- package/src/cmsView.mjs +4 -2
- package/src/cmsView.test.mjs +6 -0
- package/src/editor.css +13 -2
- package/src/examples.ts +2 -2
- package/src/fabPresence.tsx +24 -0
- package/src/googleResult.d.mts +12 -1
- package/src/googleResult.mjs +19 -2
- package/src/i18n/da-DK/apps.ts +5 -1
- package/src/i18n/da-DK/cms.ts +3 -0
- package/src/i18n/da-DK/common.ts +3 -2
- package/src/i18n/da-DK/footer.ts +26 -0
- package/src/i18n/da-DK/index.ts +2 -0
- package/src/i18n/da-DK/parts.ts +5 -0
- package/src/i18n/en-US/advanced.ts +1 -1
- package/src/i18n/en-US/apps.ts +6 -1
- package/src/i18n/en-US/cms.ts +5 -2
- package/src/i18n/en-US/common.ts +6 -4
- package/src/i18n/en-US/footer.ts +24 -0
- package/src/i18n/en-US/index.ts +2 -0
- package/src/i18n/en-US/parts.ts +8 -3
- package/src/menuChildren.ts +6 -14
- package/src/movedRoutes.mjs +12 -8
- package/src/movedRoutes.test.mjs +13 -10
- package/src/r2Caching.ts +1 -1
- package/src/registry.tsx +2 -2
- package/src/schema.ts +1 -1
- package/src/site.ts +2 -2
- package/src/statusReport.ts +24 -0
- package/src/store.ts +4 -8
- package/src/themeUi.ts +7 -7
- package/src/ui.tsx +4 -2
- package/src/useFooterClearance.ts +30 -0
- package/src/useStrayDividers.ts +24 -0
- package/theme/components/AppSlot.astro +4 -4
- package/theme/styles/global.css +10 -0
- package/theme/styles/ui.css +13 -0
- package/theme/ui/Badge.tsx +9 -3
- package/worker/cms-update.js +49 -12
- package/worker/index.js +10 -4
- package/worker/plan-status.js +1 -1
- package/worker/r2-status.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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>
|
package/src/SearchPreview.tsx
CHANGED
|
@@ -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
|
|
14
|
-
* in blue and the description, with a post's `date` before it
|
|
15
|
-
*
|
|
16
|
-
* description, at most
|
|
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={`
|
|
51
|
-
phone ? 'max-w-[360px]
|
|
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-[
|
|
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="
|
|
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
|
|
67
|
-
|
|
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>
|
package/src/SelectionParts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "
|
|
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
|
+
}
|
package/src/SideColumn.tsx
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
|
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
|
|
2
|
-
// Exposes StoreConnectedRow({ site, onDisconnected }); uses
|
|
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,
|
|
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
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|
package/src/StoreStatus.tsx
CHANGED
|
@@ -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)
|
|
2
|
-
// Exposes StoreRound, useStoreKey, useStoreConnected, StoreNotice
|
|
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
|
-
}
|
package/src/ThemeStoreScreen.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
// "
|
|
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: '#
|
|
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 "
|
|
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 "
|
|
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
|
-
/**
|
|
151
|
-
*
|
|
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
|
|
261
|
-
export
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
package/src/buildSettings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The build settings the editor writes under
|
|
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
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
|
-
|
|
80
|
+
const installed = Array.isArray(status.installed) ? status.installed : [];
|
|
81
|
+
|
|
82
|
+
return { running, checkedAt, latest, notes, installed, action, npmCommand, update, problems };
|
|
81
83
|
}
|
package/src/cmsView.test.mjs
CHANGED
|
@@ -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
|
|
25
|
-
address and 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 "
|
|
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. "
|
|
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
|
+
}
|