@morphemecms/cms 0.14.0-next-20261107093528 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -1
- package/lib/releaseNotes.d.mts +13 -11
- package/lib/releaseNotes.mjs +38 -14
- package/lib/releaseNotes.test.mjs +76 -10
- package/package.json +1 -1
- package/release-notes/i18n/da-DK/0.14.0.json +49 -0
- package/release-notes/i18n/da-DK/0.14.1.json +15 -0
- package/release-notes/i18n/en-US/0.14.0.json +49 -0
- package/release-notes/i18n/en-US/0.14.1.json +15 -0
- package/scripts/release-notes.mjs +14 -3
- package/src/CmsPage.tsx +5 -5
- package/src/MainMenu.tsx +9 -14
- package/src/bundledNotes.ts +11 -0
- package/src/cmsView.d.mts +0 -1
- package/src/cmsView.mjs +2 -4
- package/src/cmsView.test.mjs +0 -6
- package/starter/package.json +1 -1
- package/worker/cms-update.js +4 -7
- package/release-notes/i18n/da-DK/pending/apps-turn-off.json +0 -6
- package/release-notes/i18n/da-DK/pending/developer-menu.json +0 -5
- package/release-notes/i18n/da-DK/pending/dynamic-lists-page.json +0 -5
- package/release-notes/i18n/da-DK/pending/editor-footer.json +0 -6
- package/release-notes/i18n/da-DK/pending/editor-toolbar-dock.json +0 -5
- package/release-notes/i18n/da-DK/pending/google-preview-closer.json +0 -6
- package/release-notes/i18n/da-DK/pending/morpheme-connection-page.json +0 -5
- package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.json +0 -6
- package/release-notes/i18n/en-US/pending/apps-turn-off.json +0 -6
- package/release-notes/i18n/en-US/pending/developer-menu.json +0 -5
- package/release-notes/i18n/en-US/pending/dynamic-lists-page.json +0 -5
- package/release-notes/i18n/en-US/pending/editor-footer.json +0 -6
- package/release-notes/i18n/en-US/pending/editor-toolbar-dock.json +0 -5
- package/release-notes/i18n/en-US/pending/google-preview-closer.json +0 -6
- package/release-notes/i18n/en-US/pending/morpheme-connection-page.json +0 -5
- package/release-notes/i18n/en-US/pending/quiet-lamps-wander.json +0 -6
- package/src/levelStep.d.mts +0 -10
- package/src/levelStep.mjs +0 -66
- package/src/levelStep.test.mjs +0 -119
- /package/release-notes/i18n/da-DK/{pending → 0.14.0}/apps-turn-off.webp +0 -0
- /package/release-notes/i18n/da-DK/{pending → 0.14.0}/editor-footer.webp +0 -0
- /package/release-notes/i18n/da-DK/{pending → 0.14.0}/google-preview-closer.webp +0 -0
- /package/release-notes/i18n/da-DK/{pending → 0.14.0}/quiet-lamps-wander.webp +0 -0
- /package/release-notes/i18n/en-US/{pending → 0.14.0}/apps-turn-off.webp +0 -0
- /package/release-notes/i18n/en-US/{pending → 0.14.0}/editor-footer.webp +0 -0
- /package/release-notes/i18n/en-US/{pending → 0.14.0}/google-preview-closer.webp +0 -0
- /package/release-notes/i18n/en-US/{pending → 0.14.0}/quiet-lamps-wander.webp +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
-
## 0.14.
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- eea5442: On a phone, the back gesture in the open menu goes to the screen before and closes the menu, instead of going up one level in the menu.
|
|
8
|
+
- ee45cf2: Show the release notes of the running version from the installed package, not from KV.
|
|
9
|
+
|
|
10
|
+
## 0.14.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
6
13
|
|
package/lib/releaseNotes.d.mts
CHANGED
|
@@ -38,18 +38,19 @@ export function imagePath(version: string, locale: string, id: string): string;
|
|
|
38
38
|
*/
|
|
39
39
|
export function notesFor(notes: Record<string, ReleaseNote[]> | undefined, locale: string): ReleaseNote[] | undefined;
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param {string}
|
|
45
|
-
* @param {string
|
|
46
|
-
* @
|
|
41
|
+
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
42
|
+
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
43
|
+
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
44
|
+
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
45
|
+
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
46
|
+
* @param {string} version the running version
|
|
47
|
+
* @returns {{ version: string, text: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
47
48
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
49
|
+
export function bundledNotes(files: Record<string, ReleasedNotes>, images: Record<string, string>, version: string): {
|
|
50
|
+
version: string;
|
|
51
|
+
text: string;
|
|
52
|
+
notes: Record<string, ReleaseNote[]>;
|
|
53
|
+
}[];
|
|
53
54
|
/**
|
|
54
55
|
* The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
|
|
55
56
|
* per locale. Screenshots: one per card, at imagePath(). A missing or unreadable notes file leaves its locale out; a missing
|
|
@@ -102,6 +103,7 @@ export const MAX_IMAGE_BYTES: number;
|
|
|
102
103
|
/** The width of a card's screenshot in pixels (375 CSS pixels at scale 2) and its greatest height (400 CSS pixels at scale 2). */
|
|
103
104
|
export const IMAGE_WIDTH: 750;
|
|
104
105
|
export const MAX_IMAGE_HEIGHT: 800;
|
|
106
|
+
export function staleImageDirs(dirs: string[], version: string): string[];
|
|
105
107
|
export function imageKey(version: string, locale: string, id: string): string;
|
|
106
108
|
export type ReleaseNote = {
|
|
107
109
|
id: string;
|
package/lib/releaseNotes.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure helpers for release notes: checks pending notes (cards with a screenshot of the right type and size, lines), groups them per version and locale,
|
|
2
|
-
// picks a locale's notes,
|
|
2
|
+
// picks a locale's notes, builds the notes of the running minor line from the package files and reads a version's notes and screenshots from its tar.
|
|
3
3
|
// Also builds the Worker's screenshot address and KV key, parses the address and adds the address to a version's cards.
|
|
4
|
-
// Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor,
|
|
4
|
+
// Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor, bundledNotes, staleImageDirs, notesFromTar, imageUrl, imageKey,
|
|
5
5
|
// parseImagePath and withImageUrls. Depends on ./cms-version.mjs and ./changelog.mjs; no node: imports.
|
|
6
6
|
import { compareVersions } from './cms-version.mjs';
|
|
7
7
|
import { bytesFromTar, fileFromTar } from './changelog.mjs';
|
|
@@ -154,22 +154,46 @@ export function notesFor(notes, locale) {
|
|
|
154
154
|
return notes?.[locale] ?? notes?.['en-US'];
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const PLAIN = /^\d+\.\d+\.\d+$/;
|
|
158
|
+
const sameMinor = (a, b) => a.split('.').slice(0, 2).join('.') === b.split('.').slice(0, 2).join('.');
|
|
159
|
+
|
|
157
160
|
/**
|
|
158
|
-
* The
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* @param {string}
|
|
162
|
-
* @param {string
|
|
163
|
-
* @
|
|
161
|
+
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
162
|
+
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
163
|
+
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
164
|
+
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
165
|
+
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
166
|
+
* @param {string} version the running version
|
|
167
|
+
* @returns {{ version: string, text: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
164
168
|
*/
|
|
165
|
-
export function
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
export function bundledNotes(files, images, version) {
|
|
170
|
+
const byVersion = {};
|
|
171
|
+
for (const [path, file] of Object.entries(files)) {
|
|
172
|
+
const match = /\/i18n\/([^/]+)\/([^/]+)\.json$/.exec(path);
|
|
173
|
+
if (!match || !Array.isArray(file?.notes)) continue;
|
|
174
|
+
const [, locale, v] = match;
|
|
175
|
+
if (!PLAIN.test(v) || !sameMinor(v, version) || compareVersions(v, version) === 1) continue;
|
|
176
|
+
byVersion[v] ??= {};
|
|
177
|
+
byVersion[v][locale] = file.notes.map((note) => {
|
|
178
|
+
if (note?.kind !== 'card') return note;
|
|
179
|
+
const image = Object.entries(images).find(([imagePath]) => imagePath.endsWith(`/i18n/${locale}/${v}/${note.id}.webp`));
|
|
180
|
+
return image ? { ...note, image: image[1] } : note;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return Object.keys(byVersion)
|
|
184
|
+
.sort((a, b) => compareVersions(b, a))
|
|
185
|
+
.map((v) => ({ version: v, text: '', notes: byVersion[v] }));
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The screenshot folders of older minor lines: each x.y.z name whose major and minor differ from `version`'s. Never
|
|
190
|
+
* `pending` or any other name.
|
|
191
|
+
* @param {string[]} dirs names of the folders under release-notes/i18n/<locale>/
|
|
192
|
+
* @param {string} version the running version
|
|
193
|
+
* @returns {string[]}
|
|
194
|
+
*/
|
|
195
|
+
export const staleImageDirs = (dirs, version) => dirs.filter((name) => PLAIN.test(name) && !sameMinor(name, version));
|
|
196
|
+
|
|
173
197
|
/**
|
|
174
198
|
* The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
|
|
175
199
|
* per locale. Screenshots: one per card, at imagePath(). A missing or unreadable notes file leaves its locale out; a missing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Tests for lib/releaseNotes.mjs: checks pending cards and lines, groups them per version, picks a locale's notes,
|
|
2
|
-
//
|
|
1
|
+
// Tests for lib/releaseNotes.mjs: checks pending cards and lines, groups them per version, picks a locale's notes, builds
|
|
2
|
+
// the running version's notes from the package files and reads notes and screenshots from a tar.
|
|
3
3
|
// Run: node --test lib/releaseNotes.test.mjs
|
|
4
4
|
import { test } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
released,
|
|
12
12
|
imagePath,
|
|
13
13
|
notesFor,
|
|
14
|
-
|
|
14
|
+
bundledNotes,
|
|
15
|
+
staleImageDirs,
|
|
15
16
|
notesFromTar,
|
|
16
17
|
imageUrl,
|
|
17
18
|
imageKey,
|
|
@@ -163,16 +164,81 @@ test('notesFor falls back to the en-US notes', () => {
|
|
|
163
164
|
});
|
|
164
165
|
test('notesFor gives undefined without notes', () => assert.equal(notesFor(undefined, 'da-DK'), undefined));
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
const file = (version, notes) => ({ version, notes });
|
|
168
|
+
const dir = '../release-notes/i18n';
|
|
169
|
+
|
|
170
|
+
const en = (v, notes) => ({ [`${dir}/en-US/${v}.json`]: file(v, notes) });
|
|
171
|
+
|
|
172
|
+
test('bundledNotes gives the notes of both locales', () => {
|
|
173
|
+
const files = {
|
|
174
|
+
[`${dir}/en-US/0.14.0.json`]: file('0.14.0', [line('a')]),
|
|
175
|
+
[`${dir}/da-DK/0.14.0.json`]: file('0.14.0', [line('a', { text: 'Tekst' })]),
|
|
176
|
+
};
|
|
177
|
+
assert.deepEqual(bundledNotes(files, {}, '0.14.0'), [
|
|
178
|
+
{ version: '0.14.0', text: '', notes: { 'en-US': [line('a')], 'da-DK': [line('a', { text: 'Tekst' })] } },
|
|
179
|
+
]);
|
|
169
180
|
});
|
|
170
|
-
test('
|
|
171
|
-
assert.deepEqual(
|
|
181
|
+
test('bundledNotes gives the one locale that has notes', () => {
|
|
182
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), {}, '0.14.0')[0].notes, { 'en-US': [line('a')] });
|
|
183
|
+
});
|
|
184
|
+
test('bundledNotes gives the whole minor line, newest first', () => {
|
|
185
|
+
const files = { ...en('0.14.0', [line('a')]), ...en('0.14.1', [line('b')]), ...en('0.14.10', [line('c')]) };
|
|
186
|
+
assert.deepEqual(
|
|
187
|
+
bundledNotes(files, {}, '0.14.1').map((e) => e.version),
|
|
188
|
+
['0.14.1', '0.14.0'],
|
|
189
|
+
);
|
|
190
|
+
assert.deepEqual(
|
|
191
|
+
bundledNotes(files, {}, '0.14.10').map((e) => e.version),
|
|
192
|
+
['0.14.10', '0.14.1', '0.14.0'],
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
test('bundledNotes ignores another minor', () => {
|
|
196
|
+
const files = { ...en('0.13.0', [line('a')]), ...en('0.13.5', [line('b')]), ...en('0.15.0', [line('c')]), ...en('0.14.0', [line('d')]) };
|
|
197
|
+
assert.deepEqual(
|
|
198
|
+
bundledNotes(files, {}, '0.14.0').map((e) => e.version),
|
|
199
|
+
['0.14.0'],
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
test('bundledNotes ignores a newer version', () => {
|
|
203
|
+
const files = { ...en('0.14.0', [line('a')]), ...en('0.14.2', [line('b')]) };
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
bundledNotes(files, {}, '0.14.1').map((e) => e.version),
|
|
206
|
+
['0.14.0'],
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
test('bundledNotes gives an empty list without a file for the line', () => {
|
|
210
|
+
assert.deepEqual(bundledNotes(en('0.13.0', [line('a')]), {}, '0.14.0'), []);
|
|
211
|
+
assert.deepEqual(bundledNotes({}, {}, '0.14.0'), []);
|
|
212
|
+
});
|
|
213
|
+
test('bundledNotes sets the screenshot address of each version on its cards', () => {
|
|
214
|
+
const files = { ...en('0.14.0', [card('a')]), ...en('0.14.1', [card('b')]) };
|
|
215
|
+
const images = {
|
|
216
|
+
[`${dir}/en-US/0.14.0/a.webp`]: '/assets/a-hash.webp',
|
|
217
|
+
[`${dir}/en-US/0.14.1/b.webp`]: '/assets/b-hash.webp',
|
|
218
|
+
[`${dir}/da-DK/0.14.0/a.webp`]: '/assets/other.webp',
|
|
219
|
+
};
|
|
220
|
+
const result = bundledNotes(files, images, '0.14.1');
|
|
221
|
+
assert.deepEqual(result[0].notes['en-US'], [card('b', { image: '/assets/b-hash.webp' })]);
|
|
222
|
+
assert.deepEqual(result[1].notes['en-US'], [card('a', { image: '/assets/a-hash.webp' })]);
|
|
223
|
+
});
|
|
224
|
+
test('bundledNotes leaves a card without a screenshot file as it is', () => {
|
|
225
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [card('a')]), {}, '0.14.0')[0].notes['en-US'], [card('a')]);
|
|
226
|
+
});
|
|
227
|
+
test('bundledNotes leaves a line as it is', () => {
|
|
228
|
+
const images = { [`${dir}/en-US/0.14.0/a.webp`]: '/assets/a.webp' };
|
|
229
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), images, '0.14.0')[0].notes['en-US'], [line('a')]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('staleImageDirs keeps the folders of the same minor', () => {
|
|
233
|
+
assert.deepEqual(staleImageDirs(['0.14.0', '0.14.1'], '0.14.1'), []);
|
|
172
234
|
});
|
|
173
|
-
test('
|
|
174
|
-
assert.deepEqual(
|
|
235
|
+
test('staleImageDirs gives the older minors and another major', () => {
|
|
236
|
+
assert.deepEqual(staleImageDirs(['0.13.0', '0.14.0', '0.14.1', '1.14.0'], '0.15.0'), ['0.13.0', '0.14.0', '0.14.1', '1.14.0']);
|
|
237
|
+
assert.deepEqual(staleImageDirs(['0.13.2', '0.14.0', '1.14.0'], '0.14.1'), ['0.13.2', '1.14.0']);
|
|
175
238
|
});
|
|
239
|
+
test('staleImageDirs keeps pending', () => assert.deepEqual(staleImageDirs(['pending', '0.13.0'], '0.14.0'), ['0.13.0']));
|
|
240
|
+
test('staleImageDirs keeps a name that is not a version', () => assert.deepEqual(staleImageDirs(['notes', '0.13'], '0.14.0'), []));
|
|
241
|
+
test('staleImageDirs gives nothing for an empty list', () => assert.deepEqual(staleImageDirs([], '0.14.0'), []));
|
|
176
242
|
|
|
177
243
|
/** One tar entry: a 512-byte header, then the data padded to a multiple of 512. */
|
|
178
244
|
function tarEntry(name, content) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphemecms/cms",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lars Adolfsen",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.0",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "apps-turn-off",
|
|
6
|
+
"kind": "card",
|
|
7
|
+
"title": "Slå en app fra uden at miste den",
|
|
8
|
+
"text": "Nogle gange vil du sætte en app på pause uden at skulle sætte den op igen bagefter. Åbn appen under Apps, og slå Aktiveret fra lige under beskrivelsen. Indstillingerne bliver liggende, så du kan slå den til igen, når du vil."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "developer-menu",
|
|
12
|
+
"kind": "line",
|
|
13
|
+
"text": "Avanceret hedder nu Udvikler under Indstillinger, med Dele samt Caching og builds; gamle links virker stadig."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "dynamic-lists-page",
|
|
17
|
+
"kind": "line",
|
|
18
|
+
"text": "Lister hedder nu Dynamiske lister, én række i menuen, der åbner en side med alle dine lister."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "editor-footer",
|
|
22
|
+
"kind": "card",
|
|
23
|
+
"title": "Se om GitHub og Cloudflare virker",
|
|
24
|
+
"text": "Når noget ikke vil gemme eller udgive, vil du gerne vide, om problemet er din hjemmeside eller de tjenester, den kører på. Rul ned til bunden af editoren, og tryk på Driftsstatus. Den tjekker kun de tjenester, din hjemmeside bruger."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "editor-toolbar-dock",
|
|
28
|
+
"kind": "line",
|
|
29
|
+
"text": "På en telefon er værktøjslinjen til tekst slankere og holder samme afstand til kanterne."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "google-preview-closer",
|
|
33
|
+
"kind": "card",
|
|
34
|
+
"title": "En Google-forhåndsvisning, der ligner Google",
|
|
35
|
+
"text": "Du vil gerne se, hvordan en side ser ud på Google, før du udgiver den. Åbn en side eller et indlæg, og se Google-forhåndsvisningen. Den ligner nu Google på både telefon og computer, og dit vigtigste søgeord står med fed."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "morpheme-connection-page",
|
|
39
|
+
"kind": "line",
|
|
40
|
+
"text": "Din Forbindelse til Appstore ligger nu på siden Morpheme under Indstillinger."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "quiet-lamps-wander",
|
|
44
|
+
"kind": "card",
|
|
45
|
+
"title": "Se, hvad der er nyt, med billeder",
|
|
46
|
+
"text": "Opdateringer skal føles som en venlig hilsen, ikke som en udviklerlog. Åbn siden Morpheme, så ser du hver nyhed med et billede og et par almindelige ord. Alt, der er ændret i din version, står lige der."
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.1",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "drawer-back-changes-screen",
|
|
6
|
+
"kind": "line",
|
|
7
|
+
"text": "På en telefon fører et swipe tilbage med menuen åben dig nu til skærmen før og lukker menuen."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "notes-from-package",
|
|
11
|
+
"kind": "line",
|
|
12
|
+
"text": "Noterne til din version vises nu lige efter en opdatering."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.0",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "apps-turn-off",
|
|
6
|
+
"kind": "card",
|
|
7
|
+
"title": "Turn an app off without losing it",
|
|
8
|
+
"text": "Sometimes you want to pause an app without setting it up again later. Open the app under Apps and turn off Enabled, right below its description. Its settings stay, so you can turn it on again whenever you like."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "developer-menu",
|
|
12
|
+
"kind": "line",
|
|
13
|
+
"text": "Advanced is now Developer under Settings, with Parts and Caching and builds; old links still work."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "dynamic-lists-page",
|
|
17
|
+
"kind": "line",
|
|
18
|
+
"text": "Lists is now Dynamic lists, one menu row that opens a page with all your lists."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "editor-footer",
|
|
22
|
+
"kind": "card",
|
|
23
|
+
"title": "See if GitHub and Cloudflare are working",
|
|
24
|
+
"text": "When something won't save or publish, you want to know if the problem is your site or the services it runs on. Scroll to the bottom of the editor and tap System status. It checks only the services your site uses."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "editor-toolbar-dock",
|
|
28
|
+
"kind": "line",
|
|
29
|
+
"text": "On a phone, the text toolbar is slimmer and keeps an even gap to the edges."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "google-preview-closer",
|
|
33
|
+
"kind": "card",
|
|
34
|
+
"title": "A Google preview that looks like Google",
|
|
35
|
+
"text": "You want to see how a page looks in Google before you publish it. Open a page or post and look at its Google preview. It now matches Google on a phone and a computer, with your main keyword in bold."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "morpheme-connection-page",
|
|
39
|
+
"kind": "line",
|
|
40
|
+
"text": "Your Store connection now sits on the Morpheme page under Settings."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "quiet-lamps-wander",
|
|
44
|
+
"kind": "card",
|
|
45
|
+
"title": "See what's new, with pictures",
|
|
46
|
+
"text": "Updates should feel like a friendly hello, not a developer log. Open the Morpheme page and you see each new thing with a picture and a few plain words. Everything that changed in your version is right there."
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.1",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "drawer-back-changes-screen",
|
|
6
|
+
"kind": "line",
|
|
7
|
+
"text": "On a phone, swiping back with the menu open now takes you to the screen before and closes the menu."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "notes-from-package",
|
|
11
|
+
"kind": "line",
|
|
12
|
+
"text": "The notes for your version now show right after an update."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Release step: moves the pending release notes into release-notes/i18n/<locale>/<version>.json, their screenshots into
|
|
2
|
-
// <version>/<id>.webp, the version from package.json
|
|
2
|
+
// <version>/<id>.webp, the version from package.json,
|
|
3
|
+
// and deletes the screenshot folders of older minor lines (the package carries the running minor line's). Run by release:version after changeset version.
|
|
3
4
|
// Usage: node scripts/release-notes.mjs [root]; exits 1 on a problem or an existing file.
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { imagePath, problemsOf, released } from '../lib/releaseNotes.mjs';
|
|
7
|
+
import { imagePath, problemsOf, released, staleImageDirs } from '../lib/releaseNotes.mjs';
|
|
7
8
|
import { readPending, repoRoot } from './release-notes-pending.mjs';
|
|
8
9
|
|
|
9
10
|
const root = repoRoot(process.argv[2]);
|
|
@@ -36,4 +37,14 @@ for (const [locale, { file, images }] of Object.entries(result)) {
|
|
|
36
37
|
rmSync(join(localeDir, 'pending', `${id}.json`));
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
// The package carries the screenshots of the running minor line only; the notes files stay.
|
|
41
|
+
const i18n = join(root, 'release-notes', 'i18n');
|
|
42
|
+
for (const locale of readdirSync(i18n, { withFileTypes: true }).filter((e) => e.isDirectory())) {
|
|
43
|
+
const dirs = readdirSync(join(i18n, locale.name), { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
44
|
+
for (const name of staleImageDirs(
|
|
45
|
+
dirs.map((e) => e.name),
|
|
46
|
+
version,
|
|
47
|
+
))
|
|
48
|
+
rmSync(join(i18n, locale.name, name), { recursive: true });
|
|
49
|
+
}
|
|
39
50
|
console.log(`Release notes for ${version}: ${Object.keys(result).join(', ') || 'none'}.`);
|
package/src/CmsPage.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// "Morpheme" (#morpheme), under "Indstillinger": the CMS version the site runs, the notes of
|
|
2
|
-
// a newer one with its notes, why it is not taken by itself, and an update's progress, and the site's connection to the
|
|
3
|
-
// Morpheme store (MorphemeConnection) as its last group. Exposes CMS_ROUTE and the page. Depends on cmsView.mjs,
|
|
1
|
+
// "Morpheme" (#morpheme), under "Indstillinger": the CMS version the site runs, the notes of its minor line (from the
|
|
2
|
+
// package itself, bundledNotes.ts), a newer one with its notes, why it is not taken by itself, and an update's progress, and the site's connection to the
|
|
3
|
+
// Morpheme store (MorphemeConnection) as its last group. Exposes CMS_ROUTE and the page. Depends on cmsView.mjs, bundledNotes.ts,
|
|
4
4
|
// ReleaseNotes (the notes as cards), MorphemeConnection and the Worker's /api/cms.
|
|
5
5
|
import { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { CheckCircle, Clock, Info, Sparkle, WarningCircle } from '@phosphor-icons/react';
|
|
@@ -10,6 +10,7 @@ import { ReleaseNotes, type VersionNotes } from './ReleaseNotes';
|
|
|
10
10
|
import { Button, EmptyNote, Group, ListItem, ProblemNote } from './ui';
|
|
11
11
|
import { backFrom } from './site';
|
|
12
12
|
import { cmsView } from './cmsView.mjs';
|
|
13
|
+
import { runningNotes } from './bundledNotes';
|
|
13
14
|
import { snackbar } from '../lib/snackbar';
|
|
14
15
|
import { t } from './i18n';
|
|
15
16
|
import { when } from './when';
|
|
@@ -23,7 +24,6 @@ type View = {
|
|
|
23
24
|
checkedAt: string | null;
|
|
24
25
|
latest: string | null;
|
|
25
26
|
notes: VersionNotes[];
|
|
26
|
-
installed: VersionNotes[];
|
|
27
27
|
action: 'update' | 'try-again' | null;
|
|
28
28
|
npmCommand: string | null;
|
|
29
29
|
update: { state: UpdateState; to: string; at: string } | null;
|
|
@@ -107,7 +107,7 @@ export default function CmsPage() {
|
|
|
107
107
|
{!view && !problem && <EmptyNote busy>{t.app.loading}</EmptyNote>}
|
|
108
108
|
{view && (
|
|
109
109
|
<>
|
|
110
|
-
{
|
|
110
|
+
{runningNotes.length > 0 && <ReleaseNotes title={t.cms.newInYourVersion} entries={runningNotes} />}
|
|
111
111
|
<Group>
|
|
112
112
|
<ListItem icon={Info} headline={t.cms.running(view.running)} supporting={view.latest ? undefined : t.cms.upToDate} />
|
|
113
113
|
<ListItem icon={Clock} headline={view.checkedAt ? t.cms.checked(when(new Date(view.checkedAt))) : t.cms.notChecked} />
|
package/src/MainMenu.tsx
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// sections; a section opens its list, under a back row, with the open screen marked and its fold-out folded out. Apps lists
|
|
3
3
|
// Appstore and a row per app installed. From medium and wider the first level is also the collapsed navigation rail
|
|
4
4
|
// (MainRailItem), whose sections open the drawer at their list. The drawer's header is the signed-in user (AccountRow), a link
|
|
5
|
-
// to the account.
|
|
6
|
-
//
|
|
5
|
+
// to the account. The levels are not steps in the history: in the modal drawer the phone's back gesture goes to the screen
|
|
6
|
+
// before, as everywhere else, and closes the drawer. Exposes MainMenu. The tree and the open place are lib/menu.mjs
|
|
7
7
|
// (menuTree, currentPlace); it only wires them to the drawer.
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { config, header } from '@site/editor';
|
|
@@ -16,7 +16,6 @@ import { AccountRow } from './AccountRow';
|
|
|
16
16
|
import { childrenOf } from './menuChildren';
|
|
17
17
|
import { t } from './i18n';
|
|
18
18
|
import { useMenuOpen } from './menuOpen';
|
|
19
|
-
import { levelStep } from './levelStep.mjs';
|
|
20
19
|
import {
|
|
21
20
|
MainDrawer,
|
|
22
21
|
MainDrawerBack,
|
|
@@ -79,19 +78,15 @@ export function MainMenu({ route, files, user }: { route: string; files: RepoFil
|
|
|
79
78
|
setShown(null);
|
|
80
79
|
return true;
|
|
81
80
|
}, []);
|
|
82
|
-
//
|
|
81
|
+
// The modal drawer closes when the screen changes another way than by a link in it (the phone's back gesture), so the
|
|
82
|
+
// screen before is not left under it.
|
|
83
83
|
const modal = useMenuLayout().expanded !== 'standard';
|
|
84
|
-
const step = useRef<ReturnType<typeof levelStep> | null>(null);
|
|
85
84
|
useEffect(() => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
setFocus('top');
|
|
92
|
-
setShown(null);
|
|
93
|
-
});
|
|
94
|
-
}, [modal, open, shown]);
|
|
85
|
+
if (!modal || !open) return;
|
|
86
|
+
const close = () => setOpen(false);
|
|
87
|
+
window.addEventListener('hashchange', close);
|
|
88
|
+
return () => window.removeEventListener('hashchange', close);
|
|
89
|
+
}, [modal, open, setOpen]);
|
|
95
90
|
// Every level stays in the page (the screen check finds the screens from the links); the ones not shown are `hidden`.
|
|
96
91
|
const sections = tree.filter((entry) => entry.href === undefined);
|
|
97
92
|
return (
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The release notes of the minor line this package is in (x.y.0 up to its version): read at build time from the package's own release-notes/ files.
|
|
2
|
+
// Exposes runningNotes (the entries the Morpheme page shows, newest first; empty when the line has none).
|
|
3
|
+
// Depends on lib/releaseNotes.mjs (bundledNotes) and package.json's version. The globs are relative, so they work here and in a site's node_modules.
|
|
4
|
+
import { bundledNotes } from '../lib/releaseNotes.mjs';
|
|
5
|
+
import { version } from '../package.json';
|
|
6
|
+
import type { ReleasedNotes } from '../lib/releaseNotes.mjs';
|
|
7
|
+
|
|
8
|
+
const files = import.meta.glob<ReleasedNotes>('../release-notes/i18n/*/*.json', { eager: true, import: 'default' });
|
|
9
|
+
const images = import.meta.glob<string>('../release-notes/i18n/*/*/*.webp', { eager: true, query: '?url', import: 'default' });
|
|
10
|
+
|
|
11
|
+
export const runningNotes = bundledNotes(files, images, version);
|
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.
|
|
3
3
|
// Depends on cms-version for version comparison.
|
|
4
4
|
|
|
5
5
|
import { compareVersions } from '../lib/cms-version.mjs';
|
|
@@ -77,7 +77,5 @@ export function cmsView(status, now = Date.now()) {
|
|
|
77
77
|
problems.push('slow-undo');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { running, checkedAt, latest, notes, installed, action, npmCommand, update, problems };
|
|
80
|
+
return { running, checkedAt, latest, notes, action, npmCommand, update, problems };
|
|
83
81
|
}
|
package/src/cmsView.test.mjs
CHANGED
|
@@ -150,9 +150,3 @@ 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/starter/package.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@material-symbols/svg-400": "0.48.2",
|
|
33
33
|
"@material/material-color-utilities": "^0.3.0",
|
|
34
34
|
"@milkdown/kit": "^7.22.3",
|
|
35
|
-
"@morphemecms/cms": "0.
|
|
35
|
+
"@morphemecms/cms": "0.14.1",
|
|
36
36
|
"@phosphor-icons/core": "^2.1.1",
|
|
37
37
|
"@phosphor-icons/react": "^2.1.10",
|
|
38
38
|
"@tailwindcss/typography": "^0.5.20",
|
package/worker/cms-update.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Exposes RUNNING, checkForUpdate, startUpdate, watchUpdate, cmsStatus and handleCms (/api/cms, and GET /api/cms/note-image/<version>/<locale>/<id>,
|
|
4
4
|
// behind the same login check). Needs GITHUB_PUBLISH_TOKEN and the KV binding PUSH. Keeps each new version's changelog text and release
|
|
5
5
|
// notes (en-US, da-DK) as cms:notes:<version> and each card screenshot as cms:note-image:<version>:<locale>:<id>; cmsStatus returns
|
|
6
|
-
// the notes to come
|
|
6
|
+
// the notes to come, with the screenshot address on each card. Depends on github.js, session.js, push.js and lib/{lockfile,cms-version,changelog,releaseNotes}.mjs;
|
|
7
7
|
// I/O is injected (`deps`).
|
|
8
8
|
import { config } from '@site/config';
|
|
9
9
|
import { cms as cmsEn } from '../src/i18n/en-US/cms';
|
|
@@ -11,7 +11,7 @@ import { cms as cmsDa } from '../src/i18n/da-DK/cms';
|
|
|
11
11
|
import { addToLock, NeedsNpm } from '../lib/lockfile.mjs';
|
|
12
12
|
import { compareVersions, managedVersion, nextVersion, PACKAGE, watchStep } from '../lib/cms-version.mjs';
|
|
13
13
|
import { fileFromTar, gunzip, sectionsBetween } from '../lib/changelog.mjs';
|
|
14
|
-
import { imageKey,
|
|
14
|
+
import { imageKey, notesFromTar, parseImagePath, withImageUrls } from '../lib/releaseNotes.mjs';
|
|
15
15
|
import pkg from '../package.json';
|
|
16
16
|
import { GitHubError } from './files.js';
|
|
17
17
|
import { commitFiles, readFiles } from './github.js';
|
|
@@ -211,8 +211,8 @@ export async function watchUpdate(env, deps = defaultDeps, now = Date.now()) {
|
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
213
|
* What the page shows: the running version, the token, what npm said, the update, the skipped versions, the notes of the
|
|
214
|
-
* versions still to come (`notes`)
|
|
215
|
-
*
|
|
214
|
+
* versions still to come (`notes`). Each entry is `{ version, text, notes? }`; `notes` maps a locale to its release notes. The list stays
|
|
215
|
+
* empty when the running version is a pre-release. The running version's own notes are not here: the editor reads them from the package (src/bundledNotes.ts).
|
|
216
216
|
*/
|
|
217
217
|
export async function cmsStatus(env, running = RUNNING) {
|
|
218
218
|
const kv = env.PUSH;
|
|
@@ -224,7 +224,6 @@ export async function cmsStatus(env, running = RUNNING) {
|
|
|
224
224
|
update: null,
|
|
225
225
|
skipped: [],
|
|
226
226
|
notes: [],
|
|
227
|
-
installed: [],
|
|
228
227
|
};
|
|
229
228
|
const plain = /^\d+\.\d+\.\d+$/;
|
|
230
229
|
if (!kv) return status;
|
|
@@ -241,8 +240,6 @@ export async function cmsStatus(env, running = RUNNING) {
|
|
|
241
240
|
);
|
|
242
241
|
wanted.sort((a, b) => compareVersions(b, a));
|
|
243
242
|
status.notes = entries(await Promise.all(wanted.map((v) => kv.get(`cms:notes:${v}`, 'json'))));
|
|
244
|
-
const installed = installedVersions(status.update, running, kept);
|
|
245
|
-
status.installed = entries(await Promise.all(installed.map((v) => kv.get(`cms:notes:${v}`, 'json'))));
|
|
246
243
|
return status;
|
|
247
244
|
}
|
|
248
245
|
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "apps-turn-off",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "Slå en app fra uden at miste den",
|
|
5
|
-
"text": "Nogle gange vil du sætte en app på pause uden at skulle sætte den op igen bagefter. Åbn appen under Apps, og slå Aktiveret fra lige under beskrivelsen. Indstillingerne bliver liggende, så du kan slå den til igen, når du vil."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "editor-footer",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "Se om GitHub og Cloudflare virker",
|
|
5
|
-
"text": "Når noget ikke vil gemme eller udgive, vil du gerne vide, om problemet er din hjemmeside eller de tjenester, den kører på. Rul ned til bunden af editoren, og tryk på Driftsstatus. Den tjekker kun de tjenester, din hjemmeside bruger."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "google-preview-closer",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "En Google-forhåndsvisning, der ligner Google",
|
|
5
|
-
"text": "Du vil gerne se, hvordan en side ser ud på Google, før du udgiver den. Åbn en side eller et indlæg, og se Google-forhåndsvisningen. Den ligner nu Google på både telefon og computer, og dit vigtigste søgeord står med fed."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "quiet-lamps-wander",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "Se, hvad der er nyt, med billeder",
|
|
5
|
-
"text": "Opdateringer skal føles som en venlig hilsen, ikke som en udviklerlog. Åbn siden Morpheme, så ser du hver nyhed med et billede og et par almindelige ord. Alt, der er ændret i din version, står lige der."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "apps-turn-off",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "Turn an app off without losing it",
|
|
5
|
-
"text": "Sometimes you want to pause an app without setting it up again later. Open the app under Apps and turn off Enabled, right below its description. Its settings stay, so you can turn it on again whenever you like."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "editor-footer",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "See if GitHub and Cloudflare are working",
|
|
5
|
-
"text": "When something won't save or publish, you want to know if the problem is your site or the services it runs on. Scroll to the bottom of the editor and tap System status. It checks only the services your site uses."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "google-preview-closer",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "A Google preview that looks like Google",
|
|
5
|
-
"text": "You want to see how a page looks in Google before you publish it. Open a page or post and look at its Google preview. It now matches Google on a phone and a computer, with your main keyword in bold."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "quiet-lamps-wander",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "See what's new, with pictures",
|
|
5
|
-
"text": "Updates should feel like a friendly hello, not a developer log. Open the Morpheme page and you see each new thing with a picture and a few plain words. Everything that changed in your version is right there."
|
|
6
|
-
}
|
package/src/levelStep.d.mts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watches the window's history for the level's step. `set(active, toFirst)`: whether the level is shown (a step is added
|
|
3
|
-
* while it is), and what the back gesture does on it (shows the first level). `stop()` takes the step out and stops.
|
|
4
|
-
* @param {Pick<Window, 'history' | 'location' | 'addEventListener' | 'removeEventListener' | 'document'>} win
|
|
5
|
-
*/
|
|
6
|
-
export function levelStep(win: Pick<Window, "history" | "location" | "addEventListener" | "removeEventListener" | "document">): {
|
|
7
|
-
/** @param {boolean} active @param {() => void} back */
|
|
8
|
-
set(active: boolean, back: () => void): void;
|
|
9
|
-
stop(): void;
|
|
10
|
-
};
|
package/src/levelStep.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// The second level of the editor's modal main menu (MainMenu.tsx) as a step in the browser's history: while a section's
|
|
2
|
-
// list is shown in the modal drawer, the phone's back gesture (and the browser's back button) goes to the first level
|
|
3
|
-
// instead of to the screen before. The step has the same address and the same place as the screen's (history.state,
|
|
4
|
-
// nav.ts), so going back from it fires no hashchange and nav.ts never sees it. Leaving the level another way (the back
|
|
5
|
-
// row, Escape, the drawer closed) takes the step out again; a link in the drawer is followed once the step is out, so the
|
|
6
|
-
// history never keeps an empty step. Exposes levelStep. A window (the browser's; a fake one in the tests).
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Watches the window's history for the level's step. `set(active, toFirst)`: whether the level is shown (a step is added
|
|
10
|
-
* while it is), and what the back gesture does on it (shows the first level). `stop()` takes the step out and stops.
|
|
11
|
-
* @param {Pick<Window, 'history' | 'location' | 'addEventListener' | 'removeEventListener' | 'document'>} win
|
|
12
|
-
*/
|
|
13
|
-
export function levelStep(win) {
|
|
14
|
-
let pushed = false;
|
|
15
|
-
/** @type {string | null} A link's address, followed once the step is out. */
|
|
16
|
-
let pending = null;
|
|
17
|
-
let toFirst = () => {};
|
|
18
|
-
|
|
19
|
-
const leave = () => {
|
|
20
|
-
if (!pushed) return;
|
|
21
|
-
pushed = false;
|
|
22
|
-
win.history.back();
|
|
23
|
-
};
|
|
24
|
-
const onPop = () => {
|
|
25
|
-
// Back from the step: the first level, on the same screen.
|
|
26
|
-
if (pushed) {
|
|
27
|
-
pushed = false;
|
|
28
|
-
toFirst();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
// The step taken out for a link: now the link.
|
|
32
|
-
if (pending !== null) {
|
|
33
|
-
const href = pending;
|
|
34
|
-
pending = null;
|
|
35
|
-
win.location.hash = href;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
/** @param {MouseEvent} e */
|
|
39
|
-
const onClick = (e) => {
|
|
40
|
-
if (!pushed || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
41
|
-
const target = /** @type {Element | null} */ (e.target);
|
|
42
|
-
const a = target?.closest?.('a[href^="#"]');
|
|
43
|
-
if (!a || a.getAttribute('target')) return;
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
pending = a.getAttribute('href');
|
|
46
|
-
leave();
|
|
47
|
-
};
|
|
48
|
-
win.addEventListener('popstate', onPop);
|
|
49
|
-
win.document.addEventListener('click', onClick, true);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
/** @param {boolean} active @param {() => void} back */
|
|
53
|
-
set(active, back) {
|
|
54
|
-
toFirst = back;
|
|
55
|
-
if (active && !pushed) {
|
|
56
|
-
win.history.pushState({ ...(win.history.state ?? {}), level: true }, '');
|
|
57
|
-
pushed = true;
|
|
58
|
-
} else if (!active) leave();
|
|
59
|
-
},
|
|
60
|
-
stop() {
|
|
61
|
-
leave();
|
|
62
|
-
win.removeEventListener('popstate', onPop);
|
|
63
|
-
win.document.removeEventListener('click', onClick, true);
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
package/src/levelStep.test.mjs
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Tests levelStep.mjs: the modal main menu's second level as a step in the history, with a fake window whose history
|
|
2
|
-
// fires popstate as the browser's does (a step back on the same address: no hashchange).
|
|
3
|
-
// Run: node --test src/levelStep.test.mjs
|
|
4
|
-
import { test } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { levelStep } from './levelStep.mjs';
|
|
7
|
-
|
|
8
|
-
function fakeWindow(hash = '#list/pages') {
|
|
9
|
-
const listeners = { popstate: new Set(), click: new Set() };
|
|
10
|
-
const entries = [{ state: { at: 3, prev: '' }, hash }];
|
|
11
|
-
let index = 0;
|
|
12
|
-
const win = {
|
|
13
|
-
location: {
|
|
14
|
-
get hash() {
|
|
15
|
-
return entries[index].hash;
|
|
16
|
-
},
|
|
17
|
-
set hash(next) {
|
|
18
|
-
entries.splice(index + 1);
|
|
19
|
-
entries.push({ state: null, hash: next });
|
|
20
|
-
index += 1;
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
history: {
|
|
24
|
-
get state() {
|
|
25
|
-
return entries[index].state;
|
|
26
|
-
},
|
|
27
|
-
pushState(state) {
|
|
28
|
-
entries.splice(index + 1);
|
|
29
|
-
entries.push({ state, hash: entries[index].hash });
|
|
30
|
-
index += 1;
|
|
31
|
-
},
|
|
32
|
-
back() {
|
|
33
|
-
index -= 1;
|
|
34
|
-
for (const l of listeners.popstate) l();
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
addEventListener: (type, l) => listeners[type].add(l),
|
|
38
|
-
removeEventListener: (type, l) => listeners[type].delete(l),
|
|
39
|
-
document: {
|
|
40
|
-
addEventListener: (type, l) => listeners[type].add(l),
|
|
41
|
-
removeEventListener: (type, l) => listeners[type].delete(l),
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
const click = (href, extra = {}) => {
|
|
45
|
-
const a = { getAttribute: (name) => (name === 'href' ? href : (extra[name] ?? null)) };
|
|
46
|
-
const e = {
|
|
47
|
-
button: 0,
|
|
48
|
-
defaultPrevented: false,
|
|
49
|
-
target: { closest: () => a },
|
|
50
|
-
preventDefault() {
|
|
51
|
-
this.defaultPrevented = true;
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
for (const l of listeners.click) l(e);
|
|
55
|
-
return e;
|
|
56
|
-
};
|
|
57
|
-
return { win, entries: () => entries.slice(0, index + 1), click, listeners };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
test('the level shown adds a step with the same address and place', () => {
|
|
61
|
-
const { win, entries } = fakeWindow();
|
|
62
|
-
levelStep(win).set(true, () => {});
|
|
63
|
-
assert.equal(entries().length, 2);
|
|
64
|
-
assert.equal(entries()[1].hash, '#list/pages');
|
|
65
|
-
assert.deepEqual(entries()[1].state, { at: 3, prev: '', level: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('the back gesture on the level shows the first level and stays on the screen', () => {
|
|
69
|
-
const { win, entries } = fakeWindow();
|
|
70
|
-
let first = 0;
|
|
71
|
-
levelStep(win).set(true, () => (first += 1));
|
|
72
|
-
win.history.back();
|
|
73
|
-
assert.equal(first, 1);
|
|
74
|
-
assert.equal(entries().length, 1);
|
|
75
|
-
assert.equal(win.location.hash, '#list/pages');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('leaving the level another way takes the step out, without the first level called again', () => {
|
|
79
|
-
const { win, entries } = fakeWindow();
|
|
80
|
-
let first = 0;
|
|
81
|
-
const step = levelStep(win);
|
|
82
|
-
step.set(true, () => (first += 1));
|
|
83
|
-
step.set(false, () => (first += 1));
|
|
84
|
-
assert.equal(entries().length, 1);
|
|
85
|
-
assert.equal(first, 0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('a link on the level is followed after the step is out: no empty step', () => {
|
|
89
|
-
const { win, entries, click } = fakeWindow();
|
|
90
|
-
const step = levelStep(win);
|
|
91
|
-
step.set(true, () => {});
|
|
92
|
-
const e = click('#list/posts');
|
|
93
|
-
assert.equal(e.defaultPrevented, true);
|
|
94
|
-
assert.deepEqual(
|
|
95
|
-
entries().map((s) => s.hash),
|
|
96
|
-
['#list/pages', '#list/posts'],
|
|
97
|
-
);
|
|
98
|
-
// The drawer closes on the link: nothing more to take out.
|
|
99
|
-
step.set(false, () => {});
|
|
100
|
-
assert.equal(entries().length, 2);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('links are left alone at the first level and in a new tab', () => {
|
|
104
|
-
const { win, click } = fakeWindow();
|
|
105
|
-
const step = levelStep(win);
|
|
106
|
-
assert.equal(click('#list/posts').defaultPrevented, false);
|
|
107
|
-
step.set(true, () => {});
|
|
108
|
-
assert.equal(click('#list/posts', { target: '_blank' }).defaultPrevented, false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('stop takes the step out and stops listening', () => {
|
|
112
|
-
const { win, entries, listeners } = fakeWindow();
|
|
113
|
-
const step = levelStep(win);
|
|
114
|
-
step.set(true, () => {});
|
|
115
|
-
step.stop();
|
|
116
|
-
assert.equal(entries().length, 1);
|
|
117
|
-
assert.equal(listeners.popstate.size, 0);
|
|
118
|
-
assert.equal(listeners.click.size, 0);
|
|
119
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|