@morphemecms/cms 0.11.0 → 0.12.0
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 +20 -0
- package/astro.mjs +2 -1
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +56 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +2 -1
- package/scripts/check-content.mjs +2 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/App.tsx +1 -1
- package/src/AppList.tsx +24 -9
- package/src/BlockDialog.tsx +38 -17
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +7 -17
- package/src/EntryForm.tsx +1 -1
- package/src/FormFrame.tsx +4 -3
- package/src/Library.tsx +4 -11
- package/src/ListFab.tsx +13 -0
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +4 -13
- package/src/Redirects.tsx +6 -14
- package/src/Screen.tsx +13 -3
- package/src/StartFab.tsx +5 -7
- package/src/ThemeStore.tsx +33 -12
- package/src/apps.ts +2 -3
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +55 -22
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.mjs +2 -2
- package/src/movedRoutes.test.mjs +5 -0
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/store.ts +32 -27
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +26 -15
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
package/src/Redirects.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// each has, each opening a screen of its own.
|
|
3
3
|
// "Alle videresendelser" (#seo/videresendelser/alle): every address of a page the site sends on elsewhere
|
|
4
4
|
// (redirectData.ts), by address, with where it sends to, and search. Tapping one opens a sheet where
|
|
5
|
-
// where it sends to can be changed, or the redirect deleted;
|
|
5
|
+
// where it sends to can be changed, or the redirect deleted; the FAB adds a new one (in the editor's own,
|
|
6
6
|
// src/data/redirects.json). Each save is one commit, and none sends on through another afterwards
|
|
7
7
|
// (flatten): a redirect to an address that is sent on goes straight to where that ends, and those
|
|
8
8
|
// that went on through a deleted one go on to where it sent them.
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
// letter) and, over the redirects, the funnel that shows only those to one page or post, picked as a
|
|
14
14
|
// chip. The Save and Delete buttons show the loading indicator while their save runs (Forget does not).
|
|
15
15
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
-
import { ArrowRight, ArrowsDownUp, Funnel, LinkBreak,
|
|
16
|
+
import { ArrowRight, ArrowsDownUp, Funnel, LinkBreak, Signpost, Trash, X } from '@phosphor-icons/react';
|
|
17
17
|
import { addressOf, escape, REDIRECTS } from './addresses';
|
|
18
18
|
import { ask } from './ask';
|
|
19
|
+
import { CreateFab } from './CreateFab';
|
|
19
20
|
import Screen from './Screen';
|
|
20
21
|
import {
|
|
21
22
|
Button,
|
|
22
23
|
InputChip,
|
|
23
24
|
Choice,
|
|
24
25
|
Group,
|
|
25
|
-
IconButton,
|
|
26
26
|
listClass,
|
|
27
27
|
SupportingText,
|
|
28
28
|
SearchRow,
|
|
@@ -279,17 +279,6 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
|
|
|
279
279
|
? t.redirects.count(entries.length)
|
|
280
280
|
: undefined
|
|
281
281
|
}
|
|
282
|
-
actions={
|
|
283
|
-
tab === 'redirects' && (
|
|
284
|
-
<IconButton
|
|
285
|
-
icon={Plus}
|
|
286
|
-
label={t.redirects.new}
|
|
287
|
-
|
|
288
|
-
disabled={!files}
|
|
289
|
-
onClick={() => open({ from: '', to: '', isNew: true })}
|
|
290
|
-
/>
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
282
|
>
|
|
294
283
|
<p className="mb-4 text-muted">{tab === 'redirects' ? t.redirects.intro : t.redirects.missing.intro}</p>
|
|
295
284
|
<SearchRow
|
|
@@ -449,6 +438,9 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
|
|
|
449
438
|
</SheetBody>
|
|
450
439
|
)}
|
|
451
440
|
</BottomSheet>
|
|
441
|
+
{tab === 'redirects' && (
|
|
442
|
+
<CreateFab label={t.redirects.new} disabled={!files} onClick={() => open({ from: '', to: '', isNew: true })} />
|
|
443
|
+
)}
|
|
452
444
|
</Screen>
|
|
453
445
|
);
|
|
454
446
|
}
|
package/src/Screen.tsx
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// topBarSlot.tsx's place, at the top of the content), above the content. Below medium, where there is no rail, the panel
|
|
3
3
|
// button at its start opens and closes the main drawer (MainMenu.tsx, menuOpen.ts; from medium it is the rail's; not on a screen with a back arrow); at expanded and wider the logo and the site's name after it (one link to the start screen,
|
|
4
4
|
// through `back.guard`), below expanded the back arrow, the title and the status as in an app (the logo only on the start
|
|
5
|
-
// screen); at the end the actions and the notifications, on every screen, and on the start screen (`home`) the link to the
|
|
5
|
+
// screen); at the end the page actions (below expanded), the actions and the notifications, on every screen, and on the start screen (`home`) the link to the
|
|
6
6
|
// website. At expanded and wider the way here as breadcrumbs (the screen above, when that is not the start
|
|
7
|
-
// screen), the title and the status stand in a row under the bar, at the top of the content
|
|
7
|
+
// screen), the title and the status stand in a row under the bar, at the top of the content, and the page actions
|
|
8
|
+
// (`pageActions`) at the end of that row. The bar stays at the top while
|
|
8
9
|
// scrolling and keeps clear of a phone's notch. A screen's tabs (`tabs`) are the bottom of the bar, under the title, as wide
|
|
9
10
|
// as the content, and stay with it. The back arrow goes to the screen above, back in the history when that is the screen
|
|
10
11
|
// before (nav.ts up). The content is as wide as a form reads well, or wider for lists and the grid of files (`wide`) and for
|
|
@@ -38,6 +39,9 @@ type Props<T extends string> = {
|
|
|
38
39
|
back?: Back;
|
|
39
40
|
status?: ReactNode;
|
|
40
41
|
actions?: ReactNode;
|
|
42
|
+
/** The screen's own actions (a form's Preview and View on site): from expanded at the end of the row under the bar,
|
|
43
|
+
* below expanded in the top bar, before `actions`. */
|
|
44
|
+
pageActions?: ReactNode;
|
|
41
45
|
drawer?: ReactNode;
|
|
42
46
|
tabs?: TabChoice<T>;
|
|
43
47
|
home?: boolean;
|
|
@@ -74,6 +78,7 @@ export default function Screen<T extends string>({
|
|
|
74
78
|
back,
|
|
75
79
|
status,
|
|
76
80
|
actions,
|
|
81
|
+
pageActions,
|
|
77
82
|
drawer,
|
|
78
83
|
tabs,
|
|
79
84
|
home = false,
|
|
@@ -135,7 +140,10 @@ export default function Screen<T extends string>({
|
|
|
135
140
|
)}
|
|
136
141
|
</div>
|
|
137
142
|
<div className="hidden min-w-0 flex-1 expanded:block" />
|
|
138
|
-
<div className="flex shrink-0 items-center justify-end gap-1">
|
|
143
|
+
<div className="flex shrink-0 items-center justify-end gap-1">
|
|
144
|
+
{pageActions && <div className="flex items-center gap-1 expanded:hidden">{pageActions}</div>}
|
|
145
|
+
{actions}
|
|
146
|
+
</div>
|
|
139
147
|
<NotificationsLink />
|
|
140
148
|
{home && <WebsiteLink />}
|
|
141
149
|
{drawer}
|
|
@@ -169,6 +177,8 @@ export default function Screen<T extends string>({
|
|
|
169
177
|
)}
|
|
170
178
|
<h1 className="truncate font-medium">{title}</h1>
|
|
171
179
|
{status && <p className="shrink-0 text-sm text-muted">{status}</p>}
|
|
180
|
+
{/* The icons are taller than the text: the negative margin keeps the row's height. */}
|
|
181
|
+
{pageActions && <div className="-my-3 ml-auto flex shrink-0 items-center gap-1">{pageActions}</div>}
|
|
172
182
|
</div>
|
|
173
183
|
{tabs ? (
|
|
174
184
|
<TabPanel id={id} value={tabs.value}>
|
package/src/StartFab.tsx
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// The start screen's FAB menu (Start in Library.tsx): a new page and a new blog post, each opening its empty form
|
|
2
|
-
// (#ny/<list>), as the
|
|
3
|
-
// collections and icons of site.ts.
|
|
2
|
+
// (#ny/<list>), as the FAB on its list does. Exposes newOnStart (the actions) and StartFab. Uses FabMenu (ui.tsx),
|
|
3
|
+
// START_FAB (fabScreens.ts) and the collections and icons of site.ts.
|
|
4
4
|
import { Plus } from '@phosphor-icons/react';
|
|
5
5
|
import { t } from './i18n';
|
|
6
|
+
import { START_FAB } from './fabScreens';
|
|
6
7
|
import { collections, icons } from './site';
|
|
7
8
|
import { FabMenu, type FabAction } from './ui';
|
|
8
9
|
|
|
9
|
-
/** The lists
|
|
10
|
-
const ON_START = ['pages', 'blog'];
|
|
11
|
-
|
|
12
|
-
/** The start screen's actions: the lists of ON_START the site has and can make a new entry in (`create`). */
|
|
10
|
+
/** The start screen's actions: the lists of START_FAB the site has and can make a new entry in (`create`). */
|
|
13
11
|
export function newOnStart(): FabAction[] {
|
|
14
|
-
return
|
|
12
|
+
return START_FAB.flatMap((id) => {
|
|
15
13
|
const c = collections.find((x) => x.id === id);
|
|
16
14
|
return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#ny/${id}` }] : [];
|
|
17
15
|
});
|
package/src/ThemeStore.tsx
CHANGED
|
@@ -21,6 +21,8 @@ import { iconPacks, packOfTheme } from './iconPacks';
|
|
|
21
21
|
import { theme, themes, type ThemeInfo } from './themes';
|
|
22
22
|
import {
|
|
23
23
|
changedFiles,
|
|
24
|
+
legacyOf,
|
|
25
|
+
moveChanges,
|
|
24
26
|
newer,
|
|
25
27
|
notesSince,
|
|
26
28
|
STANDARD_THEME,
|
|
@@ -63,8 +65,12 @@ export function useThemeCatalog(env: Env): ShownTheme[] | null | 'failed' {
|
|
|
63
65
|
// version updated to, until the site's build has it (and the editor reloads with it, deploy.ts keepCurrent).
|
|
64
66
|
const changed = new Map<string, string>();
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const themeHere = (id: string) => (changed.has(id) ? changed.get(id) !== 'uninstalled' : themes.some((x) => x.id === id));
|
|
69
|
+
|
|
70
|
+
/** Whether a theme is in the site now: installed and not uninstalled since, or installed since; or, with its `legacy` id,
|
|
71
|
+
* a copy under that old id from the store (legacyOf). */
|
|
72
|
+
export const themeInstalled = (id: string, legacy?: string) =>
|
|
73
|
+
themeHere(id) || (!!legacy && legacyOf({ legacy }, themes) !== null && themeHere(legacy));
|
|
68
74
|
|
|
69
75
|
/** A theme installed from the store that the site's build does not have yet. */
|
|
70
76
|
export const themePending = (id: string) => changed.get(id) === 'installed' && !themes.some((x) => x.id === id);
|
|
@@ -74,7 +80,7 @@ const versionOf = (x: ThemeInfo) => (changed.has(x.id) && changed.get(x.id) !==
|
|
|
74
80
|
|
|
75
81
|
/** The newer version of a theme from the store, when the store has one. */
|
|
76
82
|
export const themeUpdateOf = (x: ThemeInfo, catalog: ShownTheme[] | null | 'failed') => {
|
|
77
|
-
const found = x.store && Array.isArray(catalog) ? catalog.find((c) => c.id === x.id) : undefined;
|
|
83
|
+
const found = x.store && Array.isArray(catalog) ? catalog.find((c) => c.id === x.id || c.legacy === x.id) : undefined;
|
|
78
84
|
return found && newer(found.version, versionOf(x)) ? found : undefined;
|
|
79
85
|
};
|
|
80
86
|
|
|
@@ -124,11 +130,13 @@ function useThemeActions(env: Env) {
|
|
|
124
130
|
text: { busy: string; done: string; commit: string; change: string },
|
|
125
131
|
next: () => Promise<Parameters<typeof themeChanges>[2]>,
|
|
126
132
|
go: (current: RepoFile[]) => Promise<boolean> = async () => true,
|
|
133
|
+
// The id the theme is installed under, when it is a legacy copy that moves to `id` (moveChanges).
|
|
134
|
+
legacy: string | null = null,
|
|
127
135
|
): Promise<boolean> {
|
|
128
136
|
setRunning({ id, action, asking: false });
|
|
129
137
|
env.notify({ text: text.busy });
|
|
130
138
|
try {
|
|
131
|
-
const [current, wanted] = await Promise.all([env.folder(themeFolder(id)), next()]);
|
|
139
|
+
const [current, wanted] = await Promise.all([env.folder(themeFolder(legacy ?? id)), next()]);
|
|
132
140
|
if (!(await go(current))) {
|
|
133
141
|
env.notify(null);
|
|
134
142
|
return false;
|
|
@@ -136,7 +144,11 @@ function useThemeActions(env: Env) {
|
|
|
136
144
|
// `go` cleared the message (and its dialog stopped the button being busy): saving goes on.
|
|
137
145
|
setRunning({ id, action, asking: false });
|
|
138
146
|
env.notify({ text: text.busy });
|
|
139
|
-
await env.commit(
|
|
147
|
+
await env.commit(
|
|
148
|
+
text.commit,
|
|
149
|
+
legacy && wanted ? moveChanges({ id }, legacy, wanted, current, env.files) : themeChanges(id, current, wanted),
|
|
150
|
+
text.change,
|
|
151
|
+
);
|
|
140
152
|
env.notify({ text: text.done, kind: 'saved' });
|
|
141
153
|
return true;
|
|
142
154
|
} catch (e) {
|
|
@@ -165,7 +177,7 @@ function useThemeActions(env: Env) {
|
|
|
165
177
|
if (done) changed.set(x.id, 'installed');
|
|
166
178
|
return done;
|
|
167
179
|
},
|
|
168
|
-
async update(x: ShownTheme, record: StoreRecord) {
|
|
180
|
+
async update(x: ShownTheme, record: StoreRecord, legacy: string | null = null) {
|
|
169
181
|
const done = await change(
|
|
170
182
|
x.id,
|
|
171
183
|
'update',
|
|
@@ -190,11 +202,15 @@ function useThemeActions(env: Env) {
|
|
|
190
202
|
record.version,
|
|
191
203
|
x.version,
|
|
192
204
|
notesSince(x.releaseNotes, record.version, x.version),
|
|
193
|
-
changedFiles(x.id, record, current, themeFolder(x.id)),
|
|
205
|
+
changedFiles(x.id, record, current, themeFolder(legacy ?? x.id)),
|
|
194
206
|
);
|
|
195
207
|
},
|
|
208
|
+
legacy,
|
|
196
209
|
);
|
|
197
|
-
if (done
|
|
210
|
+
if (done && legacy) {
|
|
211
|
+
changed.set(x.id, 'installed');
|
|
212
|
+
changed.set(legacy, 'uninstalled');
|
|
213
|
+
} else if (done) changed.set(x.id, x.version);
|
|
198
214
|
return done;
|
|
199
215
|
},
|
|
200
216
|
async uninstall(id: string, name: string) {
|
|
@@ -221,11 +237,12 @@ function useThemeActions(env: Env) {
|
|
|
221
237
|
export function allThemes(catalog: ShownTheme[] | null | 'failed') {
|
|
222
238
|
const store = Array.isArray(catalog) ? catalog : [];
|
|
223
239
|
return [
|
|
224
|
-
...store.map((x) => ({ id: x.id, name: x.name, author: x.author, version: x.version, icon: x.icon, words: x.words })),
|
|
240
|
+
...store.map((x) => ({ id: x.id, legacy: x.legacy, name: x.name, author: x.author, version: x.version, icon: x.icon, words: x.words })),
|
|
225
241
|
...themes
|
|
226
|
-
.filter((x) => !store.some((s) => s.id === x.id))
|
|
242
|
+
.filter((x) => !store.some((s) => s.id === x.id || s.legacy === x.id))
|
|
227
243
|
.map((x) => ({
|
|
228
244
|
id: x.id,
|
|
245
|
+
legacy: undefined as string | undefined,
|
|
229
246
|
name: x.name,
|
|
230
247
|
author: x.author,
|
|
231
248
|
version: x.version,
|
|
@@ -247,7 +264,7 @@ export function ThemeRow({ of }: { of: ReturnType<typeof allThemes>[number] }) {
|
|
|
247
264
|
headline={of.name}
|
|
248
265
|
supporting={t.library.themeByline(of.version, of.author)}
|
|
249
266
|
truncate
|
|
250
|
-
count={themeInstalled(of.id) && (themePending(of.id) ? t.apps.pending : t.apps.installed)}
|
|
267
|
+
count={themeInstalled(of.id, of.legacy) && (themePending(of.id) ? t.apps.pending : t.apps.installed)}
|
|
251
268
|
arrow
|
|
252
269
|
/>
|
|
253
270
|
);
|
|
@@ -363,7 +380,11 @@ export function ThemeStoreButtons({ of, env, files, top }: { of: ThemeInfo; env:
|
|
|
363
380
|
if (top)
|
|
364
381
|
return update ? (
|
|
365
382
|
<div className="mb-8">
|
|
366
|
-
<Button
|
|
383
|
+
<Button
|
|
384
|
+
disabled={actions.saving}
|
|
385
|
+
busy={actions.busy(update.id, 'update')}
|
|
386
|
+
onClick={() => actions.update(update, of.store!, legacyOf(update, themes))}
|
|
387
|
+
>
|
|
367
388
|
{t.apps.update(update.version)}
|
|
368
389
|
</Button>
|
|
369
390
|
</div>
|
package/src/apps.ts
CHANGED
|
@@ -251,9 +251,8 @@ export function kindsOffered(
|
|
|
251
251
|
export const tabsOf = (list: string, apps: SiteApp[], installed: string[]) =>
|
|
252
252
|
apps.flatMap((app) => (installed.includes(app.id) && app.tabs?.[list] ? [{ app, tab: app.tabs[list] }] : []));
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
export const appIdOf = (path: string) => path.split('/apps/')[1].split('/')[0];
|
|
254
|
+
// The id of an app from the path of a file in its folder (lib/appId.mjs, also read by the @app/self plugin).
|
|
255
|
+
export { appIdOf } from '../lib/appId.mjs';
|
|
257
256
|
|
|
258
257
|
/** The file of an app's settings (config.apps.settings/<id>.json). */
|
|
259
258
|
export const settingsFile = (id: string) => `${config.apps.settings}/${id}.json`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Whether the standard drawer shows open: remembered (sidebar.ts), panelOpen (the standard side panel is open), large (window
|
|
2
|
+
* at least `large`), reopened (the menu button opened it while the panel is open). */
|
|
3
|
+
export function drawerShown(remembered: any, panelOpen: any, large: any, reopened: any): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Whether the standard navigation drawer shows open while the standard side panel may be open beside the text (840-1199px:
|
|
2
|
+
// both do not fit). Exposes drawerShown(remembered, panelOpen, large, reopened); used by menuOpen.ts; no dependencies.
|
|
3
|
+
|
|
4
|
+
/** Whether the standard drawer shows open: remembered (sidebar.ts), panelOpen (the standard side panel is open), large (window
|
|
5
|
+
* at least `large`), reopened (the menu button opened it while the panel is open). */
|
|
6
|
+
export function drawerShown(remembered, panelOpen, large, reopened) {
|
|
7
|
+
if (!remembered && !reopened) return false;
|
|
8
|
+
if (panelOpen && !large && !reopened) return false;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tests for drawerYield.mjs: the standard drawer yields to the side panel below large, unless the menu button reopened it.
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { drawerShown } from './drawerYield.mjs';
|
|
5
|
+
|
|
6
|
+
test('the drawer shows as remembered while no panel is open', () => {
|
|
7
|
+
assert.equal(drawerShown(true, false, false, false), true);
|
|
8
|
+
assert.equal(drawerShown(false, false, true, false), false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('the drawer yields while the panel is open below large', () => {
|
|
12
|
+
assert.equal(drawerShown(true, true, false, false), false);
|
|
13
|
+
assert.equal(drawerShown(false, true, false, false), false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('the drawer shows again when the menu button reopened it', () => {
|
|
17
|
+
assert.equal(drawerShown(true, true, false, true), true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('the drawer never yields from large', () => {
|
|
21
|
+
assert.equal(drawerShown(true, true, true, false), true);
|
|
22
|
+
});
|
package/src/editor.css
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
@reference '../theme/styles/base.css';
|
|
5
5
|
|
|
6
6
|
:where(:root) {
|
|
7
|
+
/* The menus and the toolbar of the text: Material 3's menu surface and shadow (elevation level 2). */
|
|
8
|
+
--editor-menu-surface: var(--surface-container-lowest);
|
|
9
|
+
--editor-menu-shadow: var(--elevation-2);
|
|
10
|
+
|
|
7
11
|
/* How a check went (CheckList: the SEO rating), as PageSpeed shows it: the mark before a check that is fine and one
|
|
8
12
|
that can be done better, and the score's ring (from 90 good, from 50 average, else bad) with its number and the
|
|
9
13
|
light circle in it; the same in every theme. */
|
|
@@ -33,13 +37,14 @@
|
|
|
33
37
|
--editor-section-rhythm: 0.625em;
|
|
34
38
|
|
|
35
39
|
/* A form's "on this page" area (SectionPane in ui.tsx) against the window's end edge: its width (the card is as wide as the navigation drawer), the card in it
|
|
36
|
-
stands --section-pane-gap in from the end edge and from
|
|
40
|
+
stands --section-pane-gap in from the end edge and from the window's top edge. */
|
|
37
41
|
--section-pane-width: calc(var(--navigation-drawer-width, 22.5rem) + var(--section-pane-gap));
|
|
38
42
|
--section-pane-gap: 2rem;
|
|
39
43
|
|
|
40
|
-
/* The side panel of a block's fields and the section settings (
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
/* The side panel of a block's fields and the section settings (BlockDialog's standard side sheet from expanded, BottomSheet
|
|
45
|
+
`panel` in ui.tsx from large): M3's side sheet is 400dp (25rem) wide at most; where the window is narrower it is half
|
|
46
|
+
the space beside the start panel, the content column taking the other half (see below). */
|
|
47
|
+
--side-panel-width: min(25rem, calc((100vw - var(--column-start)) / 2));
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/* In dark mode the score's number in the light colours, which are read on the dark, and Google's dark colours. */
|
|
@@ -403,10 +408,25 @@
|
|
|
403
408
|
z-index: 30;
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
/* A FAB whose action runs or cannot be used now (FabAction.busy, .disabled): dimmed, and nothing to press. */
|
|
412
|
+
.editor-fab :is(.extended-fab, .fab):disabled {
|
|
413
|
+
opacity: 0.38;
|
|
414
|
+
pointer-events: none;
|
|
415
|
+
}
|
|
416
|
+
|
|
406
417
|
:root:has(.editor-fab) .snackbar-stack {
|
|
407
418
|
bottom: calc(6rem + env(safe-area-inset-bottom));
|
|
408
419
|
}
|
|
409
420
|
|
|
421
|
+
/* While a form's save bar is up (FormFrame.tsx) the FAB stands above it, so it does not cover Save; the messages above both. */
|
|
422
|
+
:root:has(.editor-save-bar) .editor-fab {
|
|
423
|
+
bottom: calc(6.5rem + env(safe-area-inset-bottom));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
:root:has(.editor-save-bar):has(.editor-fab) .snackbar-stack {
|
|
427
|
+
bottom: calc(11.5rem + env(safe-area-inset-bottom));
|
|
428
|
+
}
|
|
429
|
+
|
|
410
430
|
/* From medium the navigation rail stands at the start edge of the window (MainMenu.tsx), so the messages are in the middle of
|
|
411
431
|
the content next to it and the save bar starts where it ends; at expanded and wider, while the standard drawer is open
|
|
412
432
|
in the rail's place, they keep clear of the drawer instead. The widths are the theme's. The top bar and its tabs (Screen.tsx)
|
|
@@ -461,8 +481,8 @@
|
|
|
461
481
|
not less than nothing and not more than the space left between the panels less the column. Only when that space is
|
|
462
482
|
smaller than the column does it get narrower. (Measured against the window's width, 100vw, scrollbar and all.) Below
|
|
463
483
|
medium nothing stands beside the content and the column is centred by auto margins. While the side panel is open (see
|
|
464
|
-
the end of this block) the
|
|
465
|
-
|
|
484
|
+
the end of this block) the column is centred in the space between the start panel and the side panel by auto margins
|
|
485
|
+
(the content's end padding keeps it clear of the panel), still no wider than --column-width. */
|
|
466
486
|
:root {
|
|
467
487
|
--column-start: 0px;
|
|
468
488
|
--column-end: 0px;
|
|
@@ -504,25 +524,26 @@
|
|
|
504
524
|
right: var(--section-pane-width);
|
|
505
525
|
}
|
|
506
526
|
|
|
507
|
-
/* BottomSheet's `panel` (.editor-side-panel): the theme's modal side sheet at the window's end edge
|
|
508
|
-
the
|
|
527
|
+
/* BottomSheet's `panel` (.editor-side-panel): the theme's modal side sheet at the window's end edge, the full window
|
|
528
|
+
height like the standard one; its backdrop is clear. */
|
|
509
529
|
.editor-side-panel {
|
|
510
|
-
inset:
|
|
530
|
+
inset: 0 0 0 auto;
|
|
511
531
|
width: var(--side-panel-width);
|
|
512
|
-
height:
|
|
532
|
+
height: 100dvh;
|
|
513
533
|
}
|
|
514
534
|
.editor-side-panel:dir(rtl) {
|
|
515
|
-
inset:
|
|
535
|
+
inset: 0 auto 0 0;
|
|
516
536
|
}
|
|
517
537
|
.editor-side-panel::backdrop {
|
|
518
538
|
background: transparent;
|
|
519
539
|
}
|
|
520
540
|
}
|
|
521
541
|
|
|
522
|
-
/* The block dialog's standard side sheet (.editor-side-sheet: BlockDialog.tsx, not modal, from
|
|
523
|
-
end edge
|
|
524
|
-
|
|
525
|
-
|
|
542
|
+
/* The block dialog's standard side sheet (.editor-side-sheet: BlockDialog.tsx, not modal, from expanded) stands at the window's
|
|
543
|
+
end edge from the top of the window to its bottom (M3), with its divider the whole height. BlockDialog.tsx sets
|
|
544
|
+
[data-side-panel] on the root while it is open: the top bar (.editor-top-bar-slot, topBarSlot.tsx) ends at the sheet's
|
|
545
|
+
start edge, it takes the "on this page" area's place (that is hidden), and the content, which stays in use, keeps clear of it. */
|
|
546
|
+
@media (width >= theme(--breakpoint-expanded)) {
|
|
526
547
|
:root[data-side-panel] {
|
|
527
548
|
--column-end: var(--side-panel-width);
|
|
528
549
|
}
|
|
@@ -532,19 +553,25 @@
|
|
|
532
553
|
:root[data-side-panel] .editor-content {
|
|
533
554
|
padding-inline-end: var(--side-panel-width);
|
|
534
555
|
}
|
|
556
|
+
:root[data-side-panel] .editor-column {
|
|
557
|
+
margin-inline: auto;
|
|
558
|
+
}
|
|
535
559
|
:root[data-side-panel] .editor-save-bar {
|
|
536
560
|
right: var(--side-panel-width);
|
|
537
561
|
}
|
|
538
562
|
.editor-side-sheet {
|
|
539
563
|
position: fixed;
|
|
540
|
-
inset:
|
|
564
|
+
inset: 0 0 0 auto;
|
|
541
565
|
z-index: 20;
|
|
542
566
|
width: var(--side-panel-width);
|
|
543
567
|
max-width: none;
|
|
544
|
-
height:
|
|
568
|
+
height: 100dvh;
|
|
545
569
|
}
|
|
546
570
|
.editor-side-sheet:dir(rtl) {
|
|
547
|
-
inset:
|
|
571
|
+
inset: 0 auto 0 0;
|
|
572
|
+
}
|
|
573
|
+
:root[data-side-panel] .editor-top-bar-slot {
|
|
574
|
+
padding-inline-end: var(--side-panel-width);
|
|
548
575
|
}
|
|
549
576
|
}
|
|
550
577
|
|
|
@@ -632,9 +659,9 @@ html {
|
|
|
632
659
|
gap: 0.125rem;
|
|
633
660
|
border: 1px solid var(--line);
|
|
634
661
|
border-radius: 9999px;
|
|
635
|
-
background: var(--surface);
|
|
662
|
+
background: var(--editor-menu-surface);
|
|
636
663
|
padding: 0.25rem;
|
|
637
|
-
box-shadow: var(--shadow);
|
|
664
|
+
box-shadow: var(--editor-menu-shadow);
|
|
638
665
|
}
|
|
639
666
|
|
|
640
667
|
.editor-text .site-block-menu button {
|
|
@@ -656,6 +683,12 @@ html {
|
|
|
656
683
|
color: var(--accent);
|
|
657
684
|
}
|
|
658
685
|
|
|
686
|
+
/* The text's toolbar (MarkdownEditor.tsx, Toolbar in ui.tsx): the menu surface and shadow. */
|
|
687
|
+
.editor-tools {
|
|
688
|
+
--toolbar-standard-surface: var(--editor-menu-surface);
|
|
689
|
+
box-shadow: var(--editor-menu-shadow);
|
|
690
|
+
}
|
|
691
|
+
|
|
659
692
|
/* The icons over the selected text, on a phone at the bottom over the keyboard (selectionMenu in
|
|
660
693
|
MarkdownEditor.tsx). */
|
|
661
694
|
.editor-selection-menu {
|
|
@@ -665,9 +698,9 @@ html {
|
|
|
665
698
|
gap: 0.125rem;
|
|
666
699
|
border: 1px solid var(--line);
|
|
667
700
|
border-radius: 9999px;
|
|
668
|
-
background: var(--surface);
|
|
701
|
+
background: var(--editor-menu-surface);
|
|
669
702
|
padding: 0.25rem;
|
|
670
|
-
box-shadow: var(--shadow);
|
|
703
|
+
box-shadow: var(--editor-menu-shadow);
|
|
671
704
|
}
|
|
672
705
|
|
|
673
706
|
.editor-selection-menu[hidden],
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Tests for src/fabScreens.ts: the start screen's FAB menu and the create actions of a list. Run: node --test src/fabScreens.test.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import { START_FAB, listFab } from './fabScreens.ts';
|
|
5
|
+
|
|
6
|
+
const standard = { value: 'standard', label: 'Standard' };
|
|
7
|
+
const mailchimp = { value: 'mailchimp', label: 'Mailchimp' };
|
|
8
|
+
|
|
9
|
+
test('the start screen makes a page or a blog post', () => {
|
|
10
|
+
assert.deepEqual(START_FAB, ['pages', 'blog']);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('a list without kinds, or with one, has one action named after its create label', () => {
|
|
14
|
+
assert.deepEqual(listFab({ id: 'pages', create: 'New page' }, []), [{ label: 'New page', href: '#ny/pages' }]);
|
|
15
|
+
assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard]), [{ label: 'New promotion', href: '#ny/popups' }]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('a list of several kinds has one action per kind', () => {
|
|
19
|
+
assert.deepEqual(listFab({ id: 'popups', create: 'New promotion' }, [standard, mailchimp]), [
|
|
20
|
+
{ label: 'Standard', href: '#ny/popups/standard' },
|
|
21
|
+
{ label: 'Mailchimp', href: '#ny/popups/mailchimp' },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('a list that cannot make entries has no action', () => {
|
|
26
|
+
assert.deepEqual(listFab({ id: 'orders' }, []), []);
|
|
27
|
+
assert.deepEqual(listFab({ id: 'orders' }, [standard, mailchimp]), []);
|
|
28
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// What the create FAB offers: the start screen's menu (START_FAB, the collection ids) and a list's own (listFab: one
|
|
2
|
+
// action, or one per kind). Pure; the icons and the FAB itself are added by StartFab.tsx and ListFab.tsx.
|
|
3
|
+
|
|
4
|
+
/** The collections a new entry can be made in from the start screen, in this order: pages and blog posts. */
|
|
5
|
+
export const START_FAB = ['pages', 'blog'];
|
|
6
|
+
|
|
7
|
+
/** A list's create actions, as a label and where it goes: with several kinds one per kind (`#ny/<id>/<kind>`), else one
|
|
8
|
+
* named `create` (`#ny/<id>`); none for a list that cannot make entries (no `create`). */
|
|
9
|
+
export function listFab(
|
|
10
|
+
collection: { id: string; create?: string },
|
|
11
|
+
kinds: { value: string; label: string }[],
|
|
12
|
+
): { label: string; href: string }[] {
|
|
13
|
+
if (!collection.create) return [];
|
|
14
|
+
if (kinds.length > 1) return kinds.map((k) => ({ label: k.label, href: `#ny/${collection.id}/${k.value}` }));
|
|
15
|
+
return [{ label: collection.create, href: `#ny/${collection.id}` }];
|
|
16
|
+
}
|