@morphemecms/cms 0.14.2 → 0.14.3-next-20261107160925

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 +9 -0
  2. package/lib/license-texts/Unsplash.txt +7 -0
  3. package/lib/licenses.d.mts +8 -0
  4. package/lib/licenses.editions.test.mjs +45 -0
  5. package/lib/licenses.mjs +40 -0
  6. package/lib/releaseNotes.d.mts +79 -9
  7. package/lib/releaseNotes.mjs +136 -21
  8. package/lib/releaseNotes.test.mjs +187 -5
  9. package/package.json +1 -1
  10. package/release-notes/editions/0.14.json +12 -0
  11. package/release-notes/editions/0.14.webp +0 -0
  12. package/release-notes/i18n/da-DK/0.14.0.json +12 -11
  13. package/release-notes/i18n/da-DK/0.14.1.json +3 -2
  14. package/release-notes/i18n/da-DK/0.14.2.json +5 -4
  15. package/release-notes/i18n/da-DK/pending/edition-photo-card.json +6 -0
  16. package/release-notes/i18n/da-DK/pending/edition-photo-card.webp +0 -0
  17. package/release-notes/i18n/da-DK/pending/short-notification-buttons.json +1 -0
  18. package/release-notes/i18n/da-DK/pending/version-heading-date-photo.json +5 -0
  19. package/release-notes/i18n/en-US/0.14.0.json +13 -12
  20. package/release-notes/i18n/en-US/0.14.1.json +3 -2
  21. package/release-notes/i18n/en-US/0.14.2.json +5 -4
  22. package/release-notes/i18n/en-US/pending/edition-photo-card.json +6 -0
  23. package/release-notes/i18n/en-US/pending/edition-photo-card.webp +0 -0
  24. package/release-notes/i18n/en-US/pending/short-notification-buttons.json +1 -0
  25. package/release-notes/i18n/en-US/pending/version-heading-date-photo.json +5 -0
  26. package/scripts/release-notes.mjs +2 -1
  27. package/src/App.tsx +4 -1
  28. package/src/CmsPage.tsx +7 -2
  29. package/src/Licenses.tsx +4 -0
  30. package/src/Notifications.tsx +1 -1
  31. package/src/ReleaseNoteLines.tsx +17 -0
  32. package/src/ReleaseNoteSection.tsx +12 -0
  33. package/src/ReleaseNotes.tsx +49 -27
  34. package/src/Screen.tsx +2 -2
  35. package/src/UpdatesNotice.tsx +104 -69
  36. package/src/VersionLinePage.tsx +42 -0
  37. package/src/bundledNotes.ts +40 -5
  38. package/src/editor.css +4 -1
  39. package/src/i18n/da-DK/cms.ts +4 -0
  40. package/src/i18n/da-DK/library.ts +7 -4
  41. package/src/i18n/en-US/cms.ts +7 -0
  42. package/src/i18n/en-US/library.ts +12 -5
@@ -19,6 +19,11 @@ import {
19
19
  imageKey,
20
20
  parseImagePath,
21
21
  withImageUrls,
22
+ editionOf,
23
+ testVersionDate,
24
+ changelogSections,
25
+ minorOf,
26
+ versionLines,
22
27
  } from './releaseNotes.mjs';
23
28
 
24
29
  const card = (id, overrides = {}) => ({ id, kind: 'card', title: `Title ${id}`, text: `Text ${id}`, ...overrides });
@@ -210,11 +215,11 @@ test('bundledNotes gives the whole minor line, newest first', () => {
210
215
  ['0.14.10', '0.14.1', '0.14.0'],
211
216
  );
212
217
  });
213
- test('bundledNotes ignores another minor', () => {
218
+ test('bundledNotes keeps the older minors and leaves out newer ones', () => {
214
219
  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
220
  assert.deepEqual(
216
221
  bundledNotes(files, {}, '0.14.0').map((e) => e.version),
217
- ['0.14.0'],
222
+ ['0.14.0', '0.13.5', '0.13.0'],
218
223
  );
219
224
  });
220
225
  test('bundledNotes ignores a newer version', () => {
@@ -230,13 +235,15 @@ test('bundledNotes takes a test version (npm tag next) without throwing: the lin
230
235
  bundledNotes(files, {}, '0.14.2-next.0').map((e) => e.version),
231
236
  ['0.14.1', '0.14.0'],
232
237
  );
233
- assert.deepEqual(bundledNotes(files, {}, '0.15.0-next.3'), []);
238
+ assert.deepEqual(
239
+ bundledNotes(files, {}, '0.15.0-next.3').map((e) => e.version),
240
+ ['0.14.2', '0.14.1', '0.14.0'],
241
+ );
234
242
  });
235
243
  test('bundledNotes gives nothing for something that is not a version', () => {
236
244
  assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), {}, 'latest'), []);
237
245
  });
238
- test('bundledNotes gives an empty list without a file for the line', () => {
239
- assert.deepEqual(bundledNotes(en('0.13.0', [line('a')]), {}, '0.14.0'), []);
246
+ test('bundledNotes gives an empty list without notes or a changelog', () => {
240
247
  assert.deepEqual(bundledNotes({}, {}, '0.14.0'), []);
241
248
  });
242
249
  test('bundledNotes sets the screenshot address of each version on its cards', () => {
@@ -360,3 +367,178 @@ test('withImageUrls sets the address on cards only and leaves the input alone',
360
367
  });
361
368
 
362
369
  test('withImageUrls gives undefined without notes', () => assert.equal(withImageUrls('0.14.0', undefined), undefined));
370
+
371
+ test("editionOf gives the edition of the version's minor line, with its photo", () => {
372
+ const files = {
373
+ '../release-notes/editions/0.13.json': { name: 'Lofoten' },
374
+ '../release-notes/editions/0.14.json': {
375
+ name: ' Braies ',
376
+ place: { 'en-US': 'Lago di Braies, Italy' },
377
+ photo: { by: 'A Photographer' },
378
+ },
379
+ };
380
+ const images = {
381
+ '../release-notes/editions/0.13.webp': '/a/lofoten.webp',
382
+ '../release-notes/editions/0.14.webp': '/a/braies.webp',
383
+ };
384
+ assert.deepEqual(editionOf(files, images, '0.14.2'), {
385
+ line: '0.14',
386
+ name: 'Braies',
387
+ place: { 'en-US': 'Lago di Braies, Italy' },
388
+ by: 'A Photographer',
389
+ image: '/a/braies.webp',
390
+ });
391
+ assert.equal(editionOf(files, images, '0.14.0').name, 'Braies');
392
+ assert.equal(editionOf(files, images, '0.14.3-next-20261107145215').name, 'Braies');
393
+ assert.equal(editionOf(files, images, '0.13.4').name, 'Lofoten');
394
+ assert.deepEqual(editionOf(files, images, '0.13.4').place, {});
395
+ assert.equal(editionOf(files, images, '0.13.4').by, null);
396
+ assert.equal(editionOf(files, images, '0.15.0'), undefined);
397
+ assert.equal(editionOf(files, images, '0.12.9'), undefined);
398
+ assert.equal(editionOf(files, images, 'not a version'), undefined);
399
+ });
400
+
401
+ test('editionOf skips an edition without a name or a photo', () => {
402
+ const files = { '../release-notes/editions/0.14.json': { name: 'Braies' }, '../release-notes/editions/0.15.json': { name: '' } };
403
+ assert.equal(editionOf(files, {}, '0.14.2'), undefined);
404
+ assert.equal(editionOf(files, { '../release-notes/editions/0.15.webp': '/x.webp' }, '0.15.0'), undefined);
405
+ });
406
+
407
+ test('released dates the version when given a day', () => {
408
+ const pending = { 'en-US': { a: { note: line('a'), imageBytes: null, imageSize: null } } };
409
+ assert.equal(released(pending, '0.14.3', '2026-11-08')['en-US'].file.date, '2026-11-08');
410
+ assert.equal('date' in released(pending, '0.14.3')['en-US'].file, false);
411
+ });
412
+
413
+ test('testVersionDate reads the day from a snapshot id', () => {
414
+ assert.equal(testVersionDate('0.14.3-next-20261107145215'), '2026-11-07');
415
+ assert.equal(testVersionDate('0.14.3'), undefined);
416
+ assert.equal(testVersionDate('0.14.3-next.0'), undefined);
417
+ });
418
+
419
+ test("bundledNotes gives each released version its file's date", () => {
420
+ const files = { '../release-notes/i18n/en-US/0.14.0.json': { version: '0.14.0', date: '2026-11-07', notes: [line('a')] } };
421
+ assert.equal(bundledNotes(files, {}, '0.14.0')[0].date, '2026-11-07');
422
+ });
423
+
424
+ test("bundledNotes puts a test version's merged notes first, under its version and the day it was made", () => {
425
+ const files = { '../release-notes/i18n/en-US/0.14.2.json': { version: '0.14.2', notes: [line('a')] } };
426
+ const pending = {
427
+ '../release-notes/i18n/en-US/pending/z.json': line('z'),
428
+ '../release-notes/i18n/en-US/pending/c.json': card('c'),
429
+ '../release-notes/i18n/da-DK/pending/c.json': card('c', { title: 'Kort' }),
430
+ };
431
+ const images = { '../release-notes/i18n/en-US/pending/c.webp': '/c.webp' };
432
+ const [first, second] = bundledNotes(files, images, '0.14.3-next-20261107145215', pending);
433
+ assert.equal(first.version, '0.14.3-next-20261107145215');
434
+ assert.equal(first.date, '2026-11-07');
435
+ assert.deepEqual(
436
+ first.notes['en-US'].map((n) => n.id),
437
+ ['c', 'z'],
438
+ );
439
+ assert.equal(first.notes['en-US'][0].image, '/c.webp');
440
+ assert.equal(first.notes['da-DK'][0].title, 'Kort');
441
+ assert.equal(second.version, '0.14.2');
442
+ });
443
+
444
+ test('bundledNotes leaves out pending notes on a released version', () => {
445
+ const pending = { '../release-notes/i18n/en-US/pending/z.json': line('z') };
446
+ assert.deepEqual(bundledNotes({}, {}, '0.14.3', pending), []);
447
+ });
448
+
449
+ test("notesSince counts a test version's merged notes as new", () => {
450
+ const entries = [
451
+ { version: '0.14.3-next-20261107145215', notes: { 'en-US': [line('z')] } },
452
+ { version: '0.14.2', notes: { 'en-US': [line('a')] } },
453
+ ];
454
+ assert.deepEqual(
455
+ notesSince(entries, '0.14.2', 'en-US').map((n) => n.id),
456
+ ['z'],
457
+ );
458
+ assert.deepEqual(
459
+ notesSince(entries, '0.14.1', 'en-US').map((n) => n.id),
460
+ ['z', 'a'],
461
+ );
462
+ });
463
+
464
+ const CHANGELOG = `# @morphemecms/cms
465
+
466
+ ## 0.14.0
467
+
468
+ ### Minor Changes
469
+
470
+ - a70c310: Apps can be turned off.
471
+
472
+ ## 0.13.0
473
+
474
+ ### Minor Changes
475
+
476
+ - 1193da4: The standard parts are in English.
477
+
478
+ ### Patch Changes
479
+
480
+ - ccefd29: The licence checks know \`@app/self\`: an app that imports it no longer needs it in \`thirdParty\`, and
481
+ the site's licence list counts it as the site's own.
482
+ - 34f69c5: Open website opens in a new window.
483
+
484
+ ## 0.12.0
485
+
486
+ ## 0.11.0
487
+
488
+ - Plain line without a commit.
489
+ `;
490
+
491
+ test('changelogSections gives each version its changes as lines, newest first', () => {
492
+ const sections = changelogSections(CHANGELOG);
493
+ assert.deepEqual(
494
+ sections.map((s) => s.version),
495
+ ['0.14.0', '0.13.0', '0.12.0', '0.11.0'],
496
+ );
497
+ assert.deepEqual(
498
+ sections[1].lines.map((l) => l.text),
499
+ [
500
+ 'The standard parts are in English.',
501
+ "The licence checks know @app/self: an app that imports it no longer needs it in thirdParty, and the site's licence list counts it as the site's own.",
502
+ 'Open website opens in a new window.',
503
+ ],
504
+ );
505
+ assert.equal(sections[1].lines[0].id, '0.13.0-1193da4');
506
+ assert.equal(sections[1].lines[0].kind, 'line');
507
+ assert.deepEqual(sections[2].lines, []);
508
+ assert.equal(sections[3].lines[0].text, 'Plain line without a commit.');
509
+ });
510
+
511
+ test('bundledNotes adds the changelog versions that have no notes file, in English', () => {
512
+ const files = en('0.14.0', [line('a')]);
513
+ const entries = bundledNotes(files, {}, '0.14.0', {}, CHANGELOG);
514
+ assert.deepEqual(
515
+ entries.map((e) => e.version),
516
+ ['0.14.0', '0.13.0', '0.11.0'],
517
+ );
518
+ assert.deepEqual(entries[0].notes, { 'en-US': [line('a')] });
519
+ assert.equal(entries[1].notes['en-US'].length, 3);
520
+ assert.equal(entries[1].notes['da-DK'], undefined);
521
+ });
522
+
523
+ test('minorOf gives the minor line', () => {
524
+ assert.equal(minorOf('0.14.3-next-20261107145215'), '0.14');
525
+ assert.equal(minorOf('0.9.1'), '0.9');
526
+ });
527
+
528
+ test('versionLines groups the entries by minor line, newest line first', () => {
529
+ const lines = versionLines([
530
+ { version: '0.14.3-next-1' },
531
+ { version: '0.14.2' },
532
+ { version: '0.10.0' },
533
+ { version: '0.9.1' },
534
+ { version: '0.9.0' },
535
+ ]);
536
+ assert.deepEqual(
537
+ lines.map((l) => [l.line, l.entries.map((e) => e.version)]),
538
+ [
539
+ ['0.14', ['0.14.3-next-1', '0.14.2']],
540
+ ['0.10', ['0.10.0']],
541
+ ['0.9', ['0.9.1', '0.9.0']],
542
+ ],
543
+ );
544
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.14.2",
3
+ "version": "0.14.3-next-20261107160925",
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,12 @@
1
+ {
2
+ "name": "Braies",
3
+ "place": {
4
+ "en-US": "Lago di Braies, South Tyrol, Italy",
5
+ "da-DK": "Lago di Braies, Sydtyrol, Italien"
6
+ },
7
+ "photo": {
8
+ "by": "Pietro De Grandi",
9
+ "source": "https://images.unsplash.com/photo-1501785888041-af3ef285b470",
10
+ "license": "https://unsplash.com/license"
11
+ }
12
+ }
Binary file
@@ -1,49 +1,50 @@
1
1
  {
2
2
  "version": "0.14.0",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "apps-turn-off",
6
7
  "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."
8
+ "title": "Sæt en app på pause uden at miste indstillingerne",
9
+ "text": "Nu kan du slå en app fra og beholde dens indstillinger. Åbn appen under Apps, og slå Aktiveret fra. Slå den til igen, når du vil."
9
10
  },
10
11
  {
11
12
  "id": "developer-menu",
12
13
  "kind": "line",
13
- "text": "Avanceret hedder nu Udvikler under Indstillinger, med Dele samt Caching og builds; gamle links virker stadig."
14
+ "text": "Avanceret under Indstillinger hedder nu Udvikler, og dine gamle links virker stadig"
14
15
  },
15
16
  {
16
17
  "id": "dynamic-lists-page",
17
18
  "kind": "line",
18
- "text": "Lister hedder nu Dynamiske lister, én række i menuen, der åbner en side med alle dine lister."
19
+ "text": "Lister i menuen hedder nu Dynamiske lister og åbner en side med alle dine lister"
19
20
  },
20
21
  {
21
22
  "id": "editor-footer",
22
23
  "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."
24
+ "title": "Tjek GitHub og Cloudflare",
25
+ "text": "Hvis du ikke kan gemme eller udgive, så se, om GitHub og Cloudflare virker. Rul ned til bunden af editoren, og tryk på Driftsstatus. Den tjekker kun de tjenester, din hjemmeside bruger."
25
26
  },
26
27
  {
27
28
  "id": "editor-toolbar-dock",
28
29
  "kind": "line",
29
- "text": "På en telefon er værktøjslinjen til tekst slankere og holder samme afstand til kanterne."
30
+ "text": "På en telefon fylder værktøjslinjen til tekst mindre"
30
31
  },
31
32
  {
32
33
  "id": "google-preview-closer",
33
34
  "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."
35
+ "title": "Se din side, som Google viser den",
36
+ "text": "Forhåndsvisningen af Google-resultatet ligner nu Googles eget på både telefon og computer, og dit vigtigste søgeord står med fed. Åbn en side eller et blogindlæg, og se under Søgemaskiner."
36
37
  },
37
38
  {
38
39
  "id": "morpheme-connection-page",
39
40
  "kind": "line",
40
- "text": "Din Forbindelse til Appstore ligger nu på siden Morpheme under Indstillinger."
41
+ "text": "Forbindelse til Appstore ligger nu på siden Morpheme under Indstillinger"
41
42
  },
42
43
  {
43
44
  "id": "quiet-lamps-wander",
44
45
  "kind": "card",
45
46
  "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
+ "text": "Efter hver opdatering får du nu et kort overblik over, hvad der er nyt. Alle noterne finder du på siden Morpheme under Indstillinger."
47
48
  }
48
49
  ]
49
50
  }
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "version": "0.14.1",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "drawer-back-changes-screen",
6
7
  "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
+ "text": "Swiper du tilbage på en telefon, mens menuen er åben, lukker menuen, og du kommer til skærmen før"
8
9
  },
9
10
  {
10
11
  "id": "notes-from-package",
11
12
  "kind": "line",
12
- "text": "Noterne til din version vises nu lige efter en opdatering."
13
+ "text": "Noter som disse vises nu, lige når du har opdateret"
13
14
  }
14
15
  ]
15
16
  }
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "version": "0.14.2",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "footer-scrolls-above-fab",
6
7
  "kind": "line",
7
- "text": "På en telefon kan du nu rulle editorens bundlinje fri af plusknappen, også på korte skærme."
8
+ "text": "På en telefon skjuler plusknappen ikke længere linkene nederst i editoren"
8
9
  },
9
10
  {
10
11
  "id": "lists-follow-md3",
11
12
  "kind": "line",
12
- "text": "Lister i editoren viser nu lyse rækker på en tonet side, som i Material 3."
13
+ "text": "Lister i editoren har større tekst og skiller sig bedre ud fra siden"
13
14
  },
14
15
  {
15
16
  "id": "update-card-whats-new",
16
17
  "kind": "card",
17
- "title": "Nyhederne møder dig på startskærmen",
18
- "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."
18
+ "title": "Nyhederne står på din startskærm",
19
+ "text": "Efter en opdatering viser startskærmen øverst den nye version og de vigtigste nyheder. Tryk på Se, hvad der er nyt for at læse det hele."
19
20
  }
20
21
  ]
21
22
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "edition-photo-card",
3
+ "kind": "card",
4
+ "title": "Hver opdatering har sit eget sted",
5
+ "text": "Et lille kort øverst på startskærmen viser nu hver udgivelses foto og det sted, den er opkaldt efter. Tryk på Læs mere for at se nyhederne."
6
+ }
@@ -0,0 +1 @@
1
+ { "id": "short-notification-buttons", "kind": "line", "text": "Knapperne under Notifikationer hedder nu Ryd og Indstillinger" }
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "version-heading-date-photo",
3
+ "kind": "line",
4
+ "text": "Hver version på Morpheme-siden viser nu sin dato og sin udgaves foto"
5
+ }
@@ -1,49 +1,50 @@
1
1
  {
2
2
  "version": "0.14.0",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "apps-turn-off",
6
7
  "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."
8
+ "title": "Pause an app without losing its settings",
9
+ "text": "You can now turn an app off and keep its settings. Open the app under Apps and turn off Enabled. Turn it on again whenever you like."
9
10
  },
10
11
  {
11
12
  "id": "developer-menu",
12
13
  "kind": "line",
13
- "text": "Advanced is now Developer under Settings, with Parts and Caching and builds; old links still work."
14
+ "text": "Advanced under Settings is now called Developer, and your old links still work"
14
15
  },
15
16
  {
16
17
  "id": "dynamic-lists-page",
17
18
  "kind": "line",
18
- "text": "Lists is now Dynamic lists, one menu row that opens a page with all your lists."
19
+ "text": "Lists in the menu is now Dynamic lists and opens a page with all your lists"
19
20
  },
20
21
  {
21
22
  "id": "editor-footer",
22
23
  "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."
24
+ "title": "Check GitHub and Cloudflare",
25
+ "text": "If you can’t save or publish, see whether GitHub and Cloudflare are working. Scroll to the bottom of the editor and tap System status. It checks only the services your website uses."
25
26
  },
26
27
  {
27
28
  "id": "editor-toolbar-dock",
28
29
  "kind": "line",
29
- "text": "On a phone, the text toolbar is slimmer and keeps an even gap to the edges."
30
+ "text": "On a phone, the text toolbar takes up less room"
30
31
  },
31
32
  {
32
33
  "id": "google-preview-closer",
33
34
  "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."
35
+ "title": "See your page as Google shows it",
36
+ "text": "The Google preview now looks like the real result on a phone and a computer, with your main keyword in bold. Open a page or blog post and look under Search engines."
36
37
  },
37
38
  {
38
39
  "id": "morpheme-connection-page",
39
40
  "kind": "line",
40
- "text": "Your Store connection now sits on the Morpheme page under Settings."
41
+ "text": "Store connection is now on the Morpheme page under Settings"
41
42
  },
42
43
  {
43
44
  "id": "quiet-lamps-wander",
44
45
  "kind": "card",
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."
46
+ "title": "See what’s new",
47
+ "text": "After each update, you now get a short summary of what’s new. You’ll find all the notes on the Morpheme page under Settings."
47
48
  }
48
49
  ]
49
50
  }
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "version": "0.14.1",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "drawer-back-changes-screen",
6
7
  "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
+ "text": "On a phone, swiping back with the menu open closes the menu and takes you to the previous screen"
8
9
  },
9
10
  {
10
11
  "id": "notes-from-package",
11
12
  "kind": "line",
12
- "text": "The notes for your version now show right after an update."
13
+ "text": "Notes like these now show right after you update"
13
14
  }
14
15
  ]
15
16
  }
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "version": "0.14.2",
3
+ "date": "2026-11-07",
3
4
  "notes": [
4
5
  {
5
6
  "id": "footer-scrolls-above-fab",
6
7
  "kind": "line",
7
- "text": "On a phone, you can now scroll the editor's footer clear of the add button, also on short screens."
8
+ "text": "On a phone, the add button no longer hides the links at the bottom of the editor"
8
9
  },
9
10
  {
10
11
  "id": "lists-follow-md3",
11
12
  "kind": "line",
12
- "text": "Lists in the editor now show light rows on a tinted page, as in Material 3."
13
+ "text": "Lists in the editor have larger text and stand out more from the page"
13
14
  },
14
15
  {
15
16
  "id": "update-card-whats-new",
16
17
  "kind": "card",
17
- "title": "What's new meets you on the start screen",
18
- "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."
18
+ "title": "What’s new, right on your start screen",
19
+ "text": "After an update, the top of the start screen shows the new version and its main news. Tap See what’s new to read it all."
19
20
  }
20
21
  ]
21
22
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "edition-photo-card",
3
+ "kind": "card",
4
+ "title": "Each update has its own place",
5
+ "text": "A small card at the top of the start screen now shows each release’s photo and the place it’s named after. Tap Read more to see what’s new."
6
+ }
@@ -0,0 +1 @@
1
+ { "id": "short-notification-buttons", "kind": "line", "text": "The buttons under Notifications are now Clear and Settings" }
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "version-heading-date-photo",
3
+ "kind": "line",
4
+ "text": "Each version on the Morpheme page now shows its date and its edition’s photo"
5
+ }
@@ -17,7 +17,8 @@ if (problems.length > 0) {
17
17
  process.exit(1);
18
18
  }
19
19
 
20
- const result = released(pending, version);
20
+ // The day it is released, for the Morpheme page ("New in …" with its date).
21
+ const result = released(pending, version, new Date().toISOString().slice(0, 10));
21
22
  const targets = Object.keys(result).map((locale) => join(root, 'release-notes', 'i18n', locale, `${version}.json`));
22
23
  const existing = targets.filter((target) => existsSync(target));
23
24
  if (existing.length > 0) {
package/src/App.tsx CHANGED
@@ -33,7 +33,8 @@
33
33
  // ("Promovering"), its own and the apps' (BlockParts.tsx), and developer/parts/values the
34
34
  // theme's design tokens, "Værdier" (Tokens.tsx)
35
35
  // block-settings the blocks that can be put in, "Blokindstillinger" (BlockSettings.tsx)
36
- // morpheme "Morpheme", under "Indstillinger": the CMS's version, a newer one and its notes, an update's progress (CmsPage.tsx)
36
+ // morpheme "Morpheme", under "Indstillinger": the CMS's version, a newer one and its notes, an update's progress (CmsPage.tsx),
37
+ // and morpheme/<line> an older minor line's notes, e.g. morpheme/0.13 (VersionLinePage.tsx)
37
38
  // licenses "Licenser", under "Indstillinger": the licences of what the site and the editor are made with, and
38
39
  // licenses/<id> one with its text (Licenses.tsx)
39
40
  // status "System status", from the footer: GitHub's and Cloudflare's status (ServiceStatus.tsx)
@@ -100,6 +101,7 @@ import BrandForm from './BrandForm';
100
101
  import BlockSettings from './BlockSettings';
101
102
  import Advanced, { CACHING_ROUTE } from './Advanced';
102
103
  import CmsPage, { CMS_ROUTE } from './CmsPage';
104
+ import { VersionLinePage } from './VersionLinePage';
103
105
  import Licenses, { LICENSES_ROUTE, LicensePage } from './Licenses';
104
106
  import ServiceStatus, { STATUS_ROUTE } from './ServiceStatus';
105
107
  import { StoreThemePage } from './ThemeStore';
@@ -791,6 +793,7 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
791
793
  if (route === DEV_MENU_ROUTE) return <Developer />;
792
794
  if (route === CACHING_ROUTE) return <Advanced env={env} />;
793
795
  if (route === CMS_ROUTE) return <CmsPage />;
796
+ if (route.startsWith(`${CMS_ROUTE}/`)) return <VersionLinePage key={route} line={route.slice(CMS_ROUTE.length + 1)} />;
794
797
  if (route === LICENSES_ROUTE) return <Licenses />;
795
798
  if (route === STATUS_ROUTE) return <ServiceStatus />;
796
799
  if (route.startsWith(`${LICENSES_ROUTE}/`)) return <LicensePage key={route} id={route.slice(LICENSES_ROUTE.length + 1)} />;
package/src/CmsPage.tsx CHANGED
@@ -10,7 +10,9 @@ 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
+ import { runningLine, runningNotes } from './bundledNotes';
14
+ import { EarlierVersions } from './VersionLinePage';
15
+ import { minorOf } from '../lib/releaseNotes.mjs';
14
16
  import { snackbar } from '../lib/snackbar';
15
17
  import { t } from './i18n';
16
18
  import { when } from './when';
@@ -101,13 +103,15 @@ export default function CmsPage() {
101
103
  }
102
104
 
103
105
  const reason = view && reasonOf(view);
106
+ // The running minor line's versions; the older lines are on pages of their own (EarlierVersions).
107
+ const yourLine = runningNotes.filter((entry) => minorOf(entry.version) === runningLine);
104
108
  return (
105
109
  <Screen title={t.common.parts.morpheme} back={backFrom(CMS_ROUTE)}>
106
110
  {problem && <ProblemNote alert>{problem}</ProblemNote>}
107
111
  {!view && !problem && <EmptyNote busy>{t.app.loading}</EmptyNote>}
108
112
  {view && (
109
113
  <>
110
- {runningNotes.length > 0 && <ReleaseNotes title={t.cms.newInYourVersion} entries={runningNotes} />}
114
+ {yourLine.length > 0 && <ReleaseNotes title={t.cms.newInYourVersion} entries={yourLine} />}
111
115
  <Group>
112
116
  <ListItem icon={Info} headline={t.cms.running(view.running)} supporting={view.latest ? undefined : t.cms.upToDate} />
113
117
  <ListItem icon={Clock} headline={view.checkedAt ? t.cms.checked(when(new Date(view.checkedAt))) : t.cms.notChecked} />
@@ -126,6 +130,7 @@ export default function CmsPage() {
126
130
  {view.notes.length > 0 && <ReleaseNotes title={t.cms.whatsNew} entries={view.notes} />}
127
131
  </>
128
132
  )}
133
+ {view && <EarlierVersions />}
129
134
  <MorphemeConnection />
130
135
  </Screen>
131
136
  );
package/src/Licenses.tsx CHANGED
@@ -17,6 +17,7 @@ import { apps } from './registry';
17
17
  import { themes } from './themes';
18
18
  import { byText } from './schema';
19
19
  import { PACKAGE } from '../lib/cms-version.mjs';
20
+ import { editionPhotos } from './bundledNotes';
20
21
  import { locale, t } from './i18n';
21
22
  import {
22
23
  CMS_NAME,
@@ -242,6 +243,8 @@ export function LicensePage({ id }: { id: string }) {
242
243
  const isCms = work?.id === PACKAGE;
243
244
  const name = isCms ? t.licenses.own(CMS_NAME) : (work?.name ?? (owner ? t.licenses.own(ownerName(owner)) : t.common.parts.licenses));
244
245
  const text = work?.text ?? owner?.text;
246
+ // An edition's photo (editions/<line>, editionWorks in lib/licenses.mjs): the whole photo, uncropped.
247
+ const photo = work?.id.startsWith('editions/') ? editionPhotos[work.id.slice('editions/'.length)] : undefined;
245
248
  const back = { href: `#${LICENSES_ROUTE}`, label: t.common.parts.licenses };
246
249
  return (
247
250
  <Screen title={name} wide back={back}>
@@ -258,6 +261,7 @@ export function LicensePage({ id }: { id: string }) {
258
261
  <OwnRow owner={(work ?? owner)!} name={isCms ? CMS_NAME : ownerName(owner!)} open={false} />
259
262
  )}
260
263
  </Group>
264
+ {photo && <img src={photo} alt={work!.name} className="mb-8 block h-auto w-full rounded-xl" />}
261
265
  <section className="mb-8">
262
266
  <GroupTitle>{t.licenses.text}</GroupTitle>
263
267
  {work?.standard && <SupportingText className="mb-3 px-4">{t.licenses.standardText(work.license)}</SupportingText>}
@@ -93,7 +93,7 @@ export default function Notifications() {
93
93
  // The phone's back gesture closes the sheet only (useSheetBack).
94
94
  useSheetBack(open, closeNotifications, 'notifications');
95
95
 
96
- // Another address (a notification's link, "Vælg notifikationer") closes it.
96
+ // Another address (a notification's link, "Settings") closes it.
97
97
  useEffect(() => {
98
98
  if (!open) return;
99
99
  window.addEventListener('hashchange', closeNotifications);