@morphemecms/cms 0.14.0 → 0.14.2-next-20261107122546
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 +14 -0
- package/lib/colors.ts +4 -0
- package/lib/releaseNotes.d.mts +26 -11
- package/lib/releaseNotes.mjs +57 -14
- package/lib/releaseNotes.test.mjs +94 -10
- package/package.json +1 -1
- package/pages/editor/index.astro +8 -5
- package/release-notes/i18n/da-DK/0.14.0.json +2 -2
- package/release-notes/i18n/da-DK/0.14.1.json +15 -0
- package/release-notes/i18n/da-DK/pending/lists-follow-md3.json +5 -0
- package/release-notes/i18n/da-DK/pending/update-card-whats-new.json +6 -0
- package/release-notes/i18n/da-DK/pending/update-card-whats-new.webp +0 -0
- package/release-notes/i18n/en-US/0.14.0.json +2 -2
- package/release-notes/i18n/en-US/0.14.1.json +15 -0
- package/release-notes/i18n/en-US/pending/lists-follow-md3.json +5 -0
- package/release-notes/i18n/en-US/pending/update-card-whats-new.json +6 -0
- package/release-notes/i18n/en-US/pending/update-card-whats-new.webp +0 -0
- package/scripts/release-notes.mjs +14 -3
- package/src/CmsPage.tsx +6 -6
- package/src/MainMenu.tsx +9 -14
- package/src/ReleaseNotes.tsx +25 -26
- package/src/UpdatesNotice.tsx +99 -15
- 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/src/editor.css +11 -0
- package/src/i18n/da-DK/library.ts +4 -2
- package/src/i18n/en-US/library.ts +9 -5
- package/src/startView.d.mts +3 -2
- package/src/startView.mjs +4 -4
- package/src/startView.test.mjs +4 -4
- package/src/theme.ts +2 -2
- package/starter/package.json +1 -1
- package/theme/styles/global.css +10 -8
- package/theme/styles/ui.css +5 -1
- package/worker/cms-update.js +4 -7
- package/src/ReleaseNoteCard.tsx +0 -17
- package/src/UpdateLink.tsx +0 -21
- package/src/levelStep.d.mts +0 -10
- package/src/levelStep.mjs +0 -66
- package/src/levelStep.test.mjs +0 -119
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
+
## 0.14.2-next-20261107122546
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 13dbbca: Lists follow Material 3's list spec: the label text in body large (16/24, weight 400), also beside a switch, and the editor's rows on Surface over a page in Surface Container.
|
|
8
|
+
- 3fbb65f: The start screen's update card shows the new version and its first news, and the Morpheme page shows the release notes as an article.
|
|
9
|
+
|
|
10
|
+
## 0.14.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
- ee45cf2: Show the release notes of the running version from the installed package, not from KV.
|
|
16
|
+
|
|
3
17
|
## 0.14.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/lib/colors.ts
CHANGED
|
@@ -43,6 +43,10 @@ function declarations(set: ColorSet): string {
|
|
|
43
43
|
.join('');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** The colour of the editor's page in a mode: Surface Container, which Material 3's segmented lists stand on (editor.css),
|
|
47
|
+
* or the background when the colours have no such role. */
|
|
48
|
+
export const pageColor = (set: ColorSet): string => set['surface-container'] ?? set.bg;
|
|
49
|
+
|
|
46
50
|
/** The colours as CSS for a <style> in the <head>: light mode on :root; dark mode when the phone or
|
|
47
51
|
* computer is set to it (unless light is picked, data-theme="light" on <html>) and when dark is picked.
|
|
48
52
|
* Throws on anything that is not a colour, so a broken file never reaches a page. */
|
package/lib/releaseNotes.d.mts
CHANGED
|
@@ -38,18 +38,32 @@ 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 that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
|
|
42
|
+
* it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
|
|
43
|
+
* own version yet. Nothing when `from` is not a version.
|
|
44
|
+
* @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
|
|
45
|
+
* @param {string | undefined} from
|
|
46
|
+
* @param {string} locale
|
|
47
|
+
* @returns {ReleaseNote[]}
|
|
48
|
+
*/
|
|
49
|
+
export function notesSince(entries: {
|
|
50
|
+
version: string;
|
|
51
|
+
notes?: Record<string, ReleaseNote[]>;
|
|
52
|
+
}[], from: string | undefined, locale: string): ReleaseNote[];
|
|
53
|
+
/**
|
|
54
|
+
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
55
|
+
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
56
|
+
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
57
|
+
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
58
|
+
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
59
|
+
* @param {string} version the running version
|
|
60
|
+
* @returns {{ version: string, text: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
47
61
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
62
|
+
export function bundledNotes(files: Record<string, ReleasedNotes>, images: Record<string, string>, version: string): {
|
|
63
|
+
version: string;
|
|
64
|
+
text: string;
|
|
65
|
+
notes: Record<string, ReleaseNote[]>;
|
|
66
|
+
}[];
|
|
53
67
|
/**
|
|
54
68
|
* The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
|
|
55
69
|
* per locale. Screenshots: one per card, at imagePath(). A missing or unreadable notes file leaves its locale out; a missing
|
|
@@ -102,6 +116,7 @@ export const MAX_IMAGE_BYTES: number;
|
|
|
102
116
|
/** 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
117
|
export const IMAGE_WIDTH: 750;
|
|
104
118
|
export const MAX_IMAGE_HEIGHT: 800;
|
|
119
|
+
export function staleImageDirs(dirs: string[], version: string): string[];
|
|
105
120
|
export function imageKey(version: string, locale: string, id: string): string;
|
|
106
121
|
export type ReleaseNote = {
|
|
107
122
|
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 and those new since a version, 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, notesSince, 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,65 @@ export function notesFor(notes, locale) {
|
|
|
154
154
|
return notes?.[locale] ?? notes?.['en-US'];
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const PLAIN = /^\d+\.\d+\.\d+$/;
|
|
158
|
+
|
|
157
159
|
/**
|
|
158
|
-
* The
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* @param {string}
|
|
162
|
-
* @param {string
|
|
163
|
-
* @
|
|
160
|
+
* The notes that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
|
|
161
|
+
* it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
|
|
162
|
+
* own version yet. Nothing when `from` is not a version.
|
|
163
|
+
* @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
|
|
164
|
+
* @param {string | undefined} from
|
|
165
|
+
* @param {string} locale
|
|
166
|
+
* @returns {ReleaseNote[]}
|
|
167
|
+
*/
|
|
168
|
+
export function notesSince(entries, from, locale) {
|
|
169
|
+
const [base, pre] = String(from ?? '').split('-');
|
|
170
|
+
if (!PLAIN.test(base)) return [];
|
|
171
|
+
const notes = entries
|
|
172
|
+
.filter((entry) => PLAIN.test(entry.version) && compareVersions(entry.version, base) >= (pre === undefined ? 1 : 0))
|
|
173
|
+
.sort((a, b) => compareVersions(b.version, a.version))
|
|
174
|
+
.flatMap((entry) => notesFor(entry.notes, locale) ?? []);
|
|
175
|
+
return [...notes.filter((note) => note.kind === 'card'), ...notes.filter((note) => note.kind === 'line')];
|
|
176
|
+
}
|
|
177
|
+
const sameMinor = (a, b) => a.split('.').slice(0, 2).join('.') === b.split('.').slice(0, 2).join('.');
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
181
|
+
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
182
|
+
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
183
|
+
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
184
|
+
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
185
|
+
* @param {string} version the running version
|
|
186
|
+
* @returns {{ version: string, text: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
164
187
|
*/
|
|
165
|
-
export function
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
export function bundledNotes(files, images, version) {
|
|
189
|
+
const byVersion = {};
|
|
190
|
+
for (const [path, file] of Object.entries(files)) {
|
|
191
|
+
const match = /\/i18n\/([^/]+)\/([^/]+)\.json$/.exec(path);
|
|
192
|
+
if (!match || !Array.isArray(file?.notes)) continue;
|
|
193
|
+
const [, locale, v] = match;
|
|
194
|
+
if (!PLAIN.test(v) || !sameMinor(v, version) || compareVersions(v, version) === 1) continue;
|
|
195
|
+
byVersion[v] ??= {};
|
|
196
|
+
byVersion[v][locale] = file.notes.map((note) => {
|
|
197
|
+
if (note?.kind !== 'card') return note;
|
|
198
|
+
const image = Object.entries(images).find(([imagePath]) => imagePath.endsWith(`/i18n/${locale}/${v}/${note.id}.webp`));
|
|
199
|
+
return image ? { ...note, image: image[1] } : note;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(byVersion)
|
|
203
|
+
.sort((a, b) => compareVersions(b, a))
|
|
204
|
+
.map((v) => ({ version: v, text: '', notes: byVersion[v] }));
|
|
171
205
|
}
|
|
172
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The screenshot folders of older minor lines: each x.y.z name whose major and minor differ from `version`'s. Never
|
|
209
|
+
* `pending` or any other name.
|
|
210
|
+
* @param {string[]} dirs names of the folders under release-notes/i18n/<locale>/
|
|
211
|
+
* @param {string} version the running version
|
|
212
|
+
* @returns {string[]}
|
|
213
|
+
*/
|
|
214
|
+
export const staleImageDirs = (dirs, version) => dirs.filter((name) => PLAIN.test(name) && !sameMinor(name, version));
|
|
215
|
+
|
|
173
216
|
/**
|
|
174
217
|
* The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
|
|
175
218
|
* 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,9 @@ import {
|
|
|
11
11
|
released,
|
|
12
12
|
imagePath,
|
|
13
13
|
notesFor,
|
|
14
|
-
|
|
14
|
+
notesSince,
|
|
15
|
+
bundledNotes,
|
|
16
|
+
staleImageDirs,
|
|
15
17
|
notesFromTar,
|
|
16
18
|
imageUrl,
|
|
17
19
|
imageKey,
|
|
@@ -163,16 +165,98 @@ test('notesFor falls back to the en-US notes', () => {
|
|
|
163
165
|
});
|
|
164
166
|
test('notesFor gives undefined without notes', () => assert.equal(notesFor(undefined, 'da-DK'), undefined));
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const shipped = (version, notes) => ({ version, text: '', notes: { 'en-US': notes } });
|
|
169
|
+
const ids = (notes) => notes.map((note) => note.id);
|
|
170
|
+
|
|
171
|
+
test('notesSince gives the notes of the newer versions, newest first, cards before lines', () => {
|
|
172
|
+
const entries = [shipped('0.14.0', [card('a'), line('b')]), shipped('0.14.2', [line('c'), card('d')]), shipped('0.14.1', [line('e')])];
|
|
173
|
+
assert.deepEqual(ids(notesSince(entries, '0.14.0', 'da-DK')), ['d', 'c', 'e']);
|
|
174
|
+
assert.deepEqual(ids(notesSince(entries, '0.13.4', 'en-US')), ['d', 'a', 'c', 'e', 'b']);
|
|
175
|
+
assert.deepEqual(notesSince(entries, '0.14.2', 'en-US'), []);
|
|
176
|
+
});
|
|
177
|
+
test('notesSince counts the version of a test version as new', () => {
|
|
178
|
+
assert.deepEqual(ids(notesSince([shipped('0.14.1', [line('a')])], '0.14.1-next.2', 'en-US')), ['a']);
|
|
179
|
+
});
|
|
180
|
+
test('notesSince gives nothing without a version seen', () => {
|
|
181
|
+
assert.deepEqual(notesSince([shipped('0.14.1', [line('a')])], undefined, 'en-US'), []);
|
|
182
|
+
assert.deepEqual(notesSince([shipped('0.14.1', [line('a')])], 'latest', 'en-US'), []);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const file = (version, notes) => ({ version, notes });
|
|
186
|
+
const dir = '../release-notes/i18n';
|
|
187
|
+
|
|
188
|
+
const en = (v, notes) => ({ [`${dir}/en-US/${v}.json`]: file(v, notes) });
|
|
189
|
+
|
|
190
|
+
test('bundledNotes gives the notes of both locales', () => {
|
|
191
|
+
const files = {
|
|
192
|
+
[`${dir}/en-US/0.14.0.json`]: file('0.14.0', [line('a')]),
|
|
193
|
+
[`${dir}/da-DK/0.14.0.json`]: file('0.14.0', [line('a', { text: 'Tekst' })]),
|
|
194
|
+
};
|
|
195
|
+
assert.deepEqual(bundledNotes(files, {}, '0.14.0'), [
|
|
196
|
+
{ version: '0.14.0', text: '', notes: { 'en-US': [line('a')], 'da-DK': [line('a', { text: 'Tekst' })] } },
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
test('bundledNotes gives the one locale that has notes', () => {
|
|
200
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), {}, '0.14.0')[0].notes, { 'en-US': [line('a')] });
|
|
201
|
+
});
|
|
202
|
+
test('bundledNotes gives the whole minor line, newest first', () => {
|
|
203
|
+
const files = { ...en('0.14.0', [line('a')]), ...en('0.14.1', [line('b')]), ...en('0.14.10', [line('c')]) };
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
bundledNotes(files, {}, '0.14.1').map((e) => e.version),
|
|
206
|
+
['0.14.1', '0.14.0'],
|
|
207
|
+
);
|
|
208
|
+
assert.deepEqual(
|
|
209
|
+
bundledNotes(files, {}, '0.14.10').map((e) => e.version),
|
|
210
|
+
['0.14.10', '0.14.1', '0.14.0'],
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
test('bundledNotes ignores another minor', () => {
|
|
214
|
+
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')]) };
|
|
215
|
+
assert.deepEqual(
|
|
216
|
+
bundledNotes(files, {}, '0.14.0').map((e) => e.version),
|
|
217
|
+
['0.14.0'],
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
test('bundledNotes ignores a newer version', () => {
|
|
221
|
+
const files = { ...en('0.14.0', [line('a')]), ...en('0.14.2', [line('b')]) };
|
|
222
|
+
assert.deepEqual(
|
|
223
|
+
bundledNotes(files, {}, '0.14.1').map((e) => e.version),
|
|
224
|
+
['0.14.0'],
|
|
225
|
+
);
|
|
169
226
|
});
|
|
170
|
-
test('
|
|
171
|
-
assert.deepEqual(
|
|
227
|
+
test('bundledNotes gives an empty list without a file for the line', () => {
|
|
228
|
+
assert.deepEqual(bundledNotes(en('0.13.0', [line('a')]), {}, '0.14.0'), []);
|
|
229
|
+
assert.deepEqual(bundledNotes({}, {}, '0.14.0'), []);
|
|
230
|
+
});
|
|
231
|
+
test('bundledNotes sets the screenshot address of each version on its cards', () => {
|
|
232
|
+
const files = { ...en('0.14.0', [card('a')]), ...en('0.14.1', [card('b')]) };
|
|
233
|
+
const images = {
|
|
234
|
+
[`${dir}/en-US/0.14.0/a.webp`]: '/assets/a-hash.webp',
|
|
235
|
+
[`${dir}/en-US/0.14.1/b.webp`]: '/assets/b-hash.webp',
|
|
236
|
+
[`${dir}/da-DK/0.14.0/a.webp`]: '/assets/other.webp',
|
|
237
|
+
};
|
|
238
|
+
const result = bundledNotes(files, images, '0.14.1');
|
|
239
|
+
assert.deepEqual(result[0].notes['en-US'], [card('b', { image: '/assets/b-hash.webp' })]);
|
|
240
|
+
assert.deepEqual(result[1].notes['en-US'], [card('a', { image: '/assets/a-hash.webp' })]);
|
|
241
|
+
});
|
|
242
|
+
test('bundledNotes leaves a card without a screenshot file as it is', () => {
|
|
243
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [card('a')]), {}, '0.14.0')[0].notes['en-US'], [card('a')]);
|
|
244
|
+
});
|
|
245
|
+
test('bundledNotes leaves a line as it is', () => {
|
|
246
|
+
const images = { [`${dir}/en-US/0.14.0/a.webp`]: '/assets/a.webp' };
|
|
247
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), images, '0.14.0')[0].notes['en-US'], [line('a')]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('staleImageDirs keeps the folders of the same minor', () => {
|
|
251
|
+
assert.deepEqual(staleImageDirs(['0.14.0', '0.14.1'], '0.14.1'), []);
|
|
172
252
|
});
|
|
173
|
-
test('
|
|
174
|
-
assert.deepEqual(
|
|
253
|
+
test('staleImageDirs gives the older minors and another major', () => {
|
|
254
|
+
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']);
|
|
255
|
+
assert.deepEqual(staleImageDirs(['0.13.2', '0.14.0', '1.14.0'], '0.14.1'), ['0.13.2', '1.14.0']);
|
|
175
256
|
});
|
|
257
|
+
test('staleImageDirs keeps pending', () => assert.deepEqual(staleImageDirs(['pending', '0.13.0'], '0.14.0'), ['0.13.0']));
|
|
258
|
+
test('staleImageDirs keeps a name that is not a version', () => assert.deepEqual(staleImageDirs(['notes', '0.13'], '0.14.0'), []));
|
|
259
|
+
test('staleImageDirs gives nothing for an empty list', () => assert.deepEqual(staleImageDirs([], '0.14.0'), []));
|
|
176
260
|
|
|
177
261
|
/** One tar entry: a 512-byte header, then the data padded to a multiple of 512. */
|
|
178
262
|
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.2-next-20261107122546",
|
|
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",
|
package/pages/editor/index.astro
CHANGED
|
@@ -29,6 +29,9 @@ import { colorRoles } from '@site/colors';
|
|
|
29
29
|
// "Bevægelse" in its place at once (packages/editor/src/theme.ts).
|
|
30
30
|
import { motionCss } from '@morphemecms/cms/lib/motion';
|
|
31
31
|
import { motionScheme } from '@morphemecms/cms/site/motion';
|
|
32
|
+
import { pageColor } from '@morphemecms/cms/lib/colors';
|
|
33
|
+
|
|
34
|
+
const shown = completeColors(colors, colorRoles);
|
|
32
35
|
---
|
|
33
36
|
|
|
34
37
|
<!doctype html>
|
|
@@ -48,11 +51,11 @@ import { motionScheme } from '@morphemecms/cms/site/motion';
|
|
|
48
51
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
49
52
|
<meta name="apple-mobile-web-app-title" content={config.name} />
|
|
50
53
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
51
|
-
<!-- The top bar in the
|
|
52
|
-
own mode, for a picked one. -->
|
|
53
|
-
<meta name="theme-color" content={
|
|
54
|
-
<meta name="theme-color" content={
|
|
55
|
-
<style id="site-colors" is:inline set:html={colorsCss(
|
|
54
|
+
<!-- The top bar in the editor's page colour (pageColor), the screens' own and the app bar's at the top; data-color: that
|
|
55
|
+
of its own mode, for a picked one. -->
|
|
56
|
+
<meta name="theme-color" content={pageColor(shown.light)} data-color={pageColor(shown.light)} media="(prefers-color-scheme: light)" />
|
|
57
|
+
<meta name="theme-color" content={pageColor(shown.dark)} data-color={pageColor(shown.dark)} media="(prefers-color-scheme: dark)" />
|
|
58
|
+
<style id="site-colors" is:inline set:html={colorsCss(shown)}></style>
|
|
56
59
|
<style id="site-motion" is:inline set:html={motionCss(motionScheme)}></style>
|
|
57
60
|
<!-- Light or dark as picked on "Konto" (packages/editor/src/theme.ts), before the page is drawn. The
|
|
58
61
|
language (lang) is the site's until the editor has picked its own (packages/editor/src/i18n/). -->
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
{
|
|
43
43
|
"id": "quiet-lamps-wander",
|
|
44
44
|
"kind": "card",
|
|
45
|
-
"title": "Se, hvad der er nyt
|
|
46
|
-
"text": "
|
|
45
|
+
"title": "Se, hvad der er nyt",
|
|
46
|
+
"text": "Det kan være svært at følge med i, hvad der er nyt i Morpheme. Derfor får du nu et kort overblik over de nye funktioner efter hver opdatering. Du finder det hele samlet på siden Morpheme."
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
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,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "update-card-whats-new",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "Nyhederne møder dig på startskærmen",
|
|
5
|
+
"text": "Efter en opdatering skal du ikke selv lede efter, hvad der er nyt. Øverst på startskærmen ser du nu den nye version og de vigtigste nyheder, med et billede af den største. Tryk på Se, hvad der er nyt for at se det hele."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
{
|
|
43
43
|
"id": "quiet-lamps-wander",
|
|
44
44
|
"kind": "card",
|
|
45
|
-
"title": "See what's new
|
|
46
|
-
"text": "
|
|
45
|
+
"title": "See what's new",
|
|
46
|
+
"text": "It can be hard to keep up with what's new in Morpheme. So after each update, you now get a short overview of the new features. You'll find it all in one place on the Morpheme page."
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
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
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "update-card-whats-new",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "What's new meets you on the start screen",
|
|
5
|
+
"text": "After an update, you shouldn't have to look for what's new yourself. The top of the start screen now shows the new version and the main news, with a picture of the biggest one. Tap See what's new to see it all."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -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,7 +1,7 @@
|
|
|
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,
|
|
4
|
-
// ReleaseNotes (the notes as
|
|
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
|
+
// ReleaseNotes (the notes as an article), 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';
|
|
7
7
|
import Screen from './Screen';
|
|
@@ -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 (
|