@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/colors.ts +4 -0
  3. package/lib/releaseNotes.d.mts +26 -11
  4. package/lib/releaseNotes.mjs +57 -14
  5. package/lib/releaseNotes.test.mjs +94 -10
  6. package/package.json +1 -1
  7. package/pages/editor/index.astro +8 -5
  8. package/release-notes/i18n/da-DK/0.14.0.json +2 -2
  9. package/release-notes/i18n/da-DK/0.14.1.json +15 -0
  10. package/release-notes/i18n/da-DK/pending/lists-follow-md3.json +5 -0
  11. package/release-notes/i18n/da-DK/pending/update-card-whats-new.json +6 -0
  12. package/release-notes/i18n/da-DK/pending/update-card-whats-new.webp +0 -0
  13. package/release-notes/i18n/en-US/0.14.0.json +2 -2
  14. package/release-notes/i18n/en-US/0.14.1.json +15 -0
  15. package/release-notes/i18n/en-US/pending/lists-follow-md3.json +5 -0
  16. package/release-notes/i18n/en-US/pending/update-card-whats-new.json +6 -0
  17. package/release-notes/i18n/en-US/pending/update-card-whats-new.webp +0 -0
  18. package/scripts/release-notes.mjs +14 -3
  19. package/src/CmsPage.tsx +6 -6
  20. package/src/MainMenu.tsx +9 -14
  21. package/src/ReleaseNotes.tsx +25 -26
  22. package/src/UpdatesNotice.tsx +99 -15
  23. package/src/bundledNotes.ts +11 -0
  24. package/src/cmsView.d.mts +0 -1
  25. package/src/cmsView.mjs +2 -4
  26. package/src/cmsView.test.mjs +0 -6
  27. package/src/editor.css +11 -0
  28. package/src/i18n/da-DK/library.ts +4 -2
  29. package/src/i18n/en-US/library.ts +9 -5
  30. package/src/startView.d.mts +3 -2
  31. package/src/startView.mjs +4 -4
  32. package/src/startView.test.mjs +4 -4
  33. package/src/theme.ts +2 -2
  34. package/starter/package.json +1 -1
  35. package/theme/styles/global.css +10 -8
  36. package/theme/styles/ui.css +5 -1
  37. package/worker/cms-update.js +4 -7
  38. package/src/ReleaseNoteCard.tsx +0 -17
  39. package/src/UpdateLink.tsx +0 -21
  40. package/src/levelStep.d.mts +0 -10
  41. package/src/levelStep.mjs +0 -66
  42. 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. */
@@ -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 kept versions an update brought, newest first. After a finished update to the running version: the versions after
42
- * `from` up to the running one. Else: the running version alone, when it is kept.
43
- * @param {{ from?: string, to: string, state: string } | null} update
44
- * @param {string} running
45
- * @param {string[]} kept versions that have notes in KV
46
- * @returns {string[]}
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 installedVersions(update: {
49
- from?: string;
50
- to: string;
51
- state: string;
52
- } | null, running: string, kept: string[]): string[];
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;
@@ -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, lists the versions an update brought and reads a version's notes and screenshots from its tar.
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, installedVersions, notesFromTar, imageUrl, imageKey,
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 kept versions an update brought, newest first. After a finished update to the running version: the versions after
159
- * `from` up to the running one. Else: the running version alone, when it is kept.
160
- * @param {{ from?: string, to: string, state: string } | null} update
161
- * @param {string} running
162
- * @param {string[]} kept versions that have notes in KV
163
- * @returns {string[]}
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 installedVersions(update, running, kept) {
166
- const finished = update?.state === 'done' && update.to === running && typeof update.from === 'string';
167
- const versions = finished
168
- ? kept.filter((version) => compareVersions(version, update.from) === 1 && compareVersions(version, running) <= 0)
169
- : kept.filter((version) => version === running);
170
- return versions.sort((a, b) => compareVersions(b, a));
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, lists
2
- // installed versions and reads notes and screenshots from a tar.
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
- installedVersions,
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
- test('installedVersions gives the finished update range, newest first', () => {
167
- const update = { from: '0.13.0', to: '0.14.1', state: 'done' };
168
- assert.deepEqual(installedVersions(update, '0.14.1', ['0.14.0', '0.14.1', '0.15.0']), ['0.14.1', '0.14.0']);
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('installedVersions gives only the running version when the update went to another version', () => {
171
- assert.deepEqual(installedVersions({ to: '0.14.0', state: 'done' }, '0.14.1', ['0.14.1']), ['0.14.1']);
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('installedVersions gives only the running version, and only when it is kept', () => {
174
- assert.deepEqual(installedVersions(null, '0.14.1', ['0.14.0']), []);
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.0",
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",
@@ -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 background's colour, the screens' own and the app bar's at the top; data-color: that of its
52
- own mode, for a picked one. -->
53
- <meta name="theme-color" content={colors.light.bg} data-color={colors.light.bg} media="(prefers-color-scheme: light)" />
54
- <meta name="theme-color" content={colors.dark.bg} data-color={colors.dark.bg} media="(prefers-color-scheme: dark)" />
55
- <style id="site-colors" is:inline set:html={colorsCss(completeColors(colors, colorRoles))}></style>
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, 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."
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,5 @@
1
+ {
2
+ "id": "lists-follow-md3",
3
+ "kind": "line",
4
+ "text": "Lister i editoren viser nu lyse rækker på en tonet side, som i Material 3."
5
+ }
@@ -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
+ }
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "id": "quiet-lamps-wander",
44
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."
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,5 @@
1
+ {
2
+ "id": "lists-follow-md3",
3
+ "kind": "line",
4
+ "text": "Lists in the editor now show light rows on a tinted page, as in Material 3."
5
+ }
@@ -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
+ }
@@ -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. Run by release:version after changeset version.
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 the update just installed,
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 cards), MorphemeConnection and the Worker's /api/cms.
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
- {view.installed.length > 0 && <ReleaseNotes title={t.cms.newInYourVersion} entries={view.installed} />}
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. In the modal drawer a section's list is a step in the history (levelStep.mjs): the phone's back gesture
6
- // goes from it to the first level. Exposes MainMenu. The tree and the open place are lib/menu.mjs
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
- // A section's list in the modal drawer: the back gesture goes to the first level (levelStep.mjs), not to the screen before.
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
- step.current = levelStep(window);
87
- return () => step.current?.stop();
88
- }, []);
89
- useEffect(() => {
90
- step.current?.set(modal && open && shown !== null, () => {
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 (