@morphemecms/cms 0.14.2 → 0.14.3-next-20261107145215

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @morphemecms/cms
2
2
 
3
+ ## 0.14.3-next-20261107145215
4
+
5
+ ### Patch Changes
6
+
7
+ - ed6e436: The release notes on the Morpheme page and the start screen are rewritten in plainer words, in English and Danish.
8
+ - aa478c9: After an update, the start screen shows a small card with the edition's photo and name instead of the large list of news.
9
+
3
10
  ## 0.14.2
4
11
 
5
12
  ### Patch Changes
@@ -0,0 +1,7 @@
1
+ Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to
2
+ download, copy, modify, distribute, perform, and use photos from Unsplash for free,
3
+ including for commercial purposes, without permission from or attributing the photographer
4
+ or Unsplash. This license does not include the right to compile photos from Unsplash to
5
+ replicate a similar or competing service.
6
+
7
+ https://unsplash.com/license
@@ -98,6 +98,12 @@ export function declarationProblems({ license, thirdParty, folder, where, what,
98
98
  * checked with what it gives in place of what a package lacks (declarationProblems); those they depend on are not.
99
99
  * @param {string} root @param {string[]} folders @param {Set<string>} [skip] */
100
100
  export function codePackageProblems(root: string, folders: string[], skip?: Set<string>): any[];
101
+ /** The photo of each edition the CMS carries (release-notes/editions/<version>.json and .webp, shown on the start screen
102
+ * after an update) as a work in "Licenser": a picture in the editor, its id `editions/<version>`, named after its place
103
+ * (en-US), by its photographer under the Unsplash License (license-texts/Unsplash.txt). An edition without a photographer
104
+ * gives a work without an author, which workProblems stops the build on.
105
+ * @param {string} cmsRoot the CMS package's folder @returns {object[]} */
106
+ export function editionWorks(cmsRoot: string): object[];
101
107
  /** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
102
108
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
103
109
  * (an app installed; the theme in use, and the standard theme it builds on).
@@ -147,6 +153,8 @@ export const UNLICENSED: "UNLICENSED";
147
153
  export const OWN_LICENSE_FILE: "LICENSE";
148
154
  export function packageName(specifier: any): any;
149
155
  export function ownerId(kind: any, id: any): string;
156
+ /** The licence of the editions' photos (release-notes/editions/), as "Licenser" shows it. */
157
+ export const UNSPLASH_LICENSE: "Unsplash License";
150
158
  /**
151
159
  * An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
152
160
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
@@ -0,0 +1,45 @@
1
+ // Tests editionWorks in licenses.mjs: each edition's photo as a picture in the editor, under the Unsplash License.
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { editionWorks, UNSPLASH_LICENSE, workProblems } from './licenses.mjs';
8
+
9
+ const root = (editions) => {
10
+ const dir = mkdtempSync(join(tmpdir(), 'editions-'));
11
+ mkdirSync(join(dir, 'release-notes', 'editions'), { recursive: true });
12
+ for (const [version, edition] of Object.entries(editions))
13
+ writeFileSync(join(dir, 'release-notes', 'editions', `${version}.json`), JSON.stringify(edition));
14
+ return dir;
15
+ };
16
+
17
+ test("an edition's photo is a picture in the editor, by its photographer, under the Unsplash License", () => {
18
+ const [work] = editionWorks(
19
+ root({ '0.14.2': { name: 'Braies', place: { 'en-US': 'Lago di Braies, Italy' }, photo: { by: 'A Photographer' } } }),
20
+ );
21
+ assert.equal(work.id, 'editions/0.14.2');
22
+ assert.equal(work.name, 'Lago di Braies, Italy');
23
+ assert.equal(work.author, 'A Photographer');
24
+ assert.equal(work.license, UNSPLASH_LICENSE);
25
+ assert.equal(work.kind, 'image');
26
+ assert.deepEqual(work.where, ['editor']);
27
+ assert.match(work.text, /^Unsplash grants you an irrevocable/);
28
+ assert.deepEqual(workProblems(work, 'x'), []);
29
+ });
30
+
31
+ test('an edition without a photographer is a problem', () => {
32
+ const [work] = editionWorks(root({ '0.14.2': { name: 'Braies' } }));
33
+ assert.equal(work.name, 'Braies');
34
+ assert.equal(workProblems(work, 'x').length, 1);
35
+ });
36
+
37
+ test('no editions folder gives no works', () => {
38
+ assert.deepEqual(editionWorks(mkdtempSync(join(tmpdir(), 'none-'))), []);
39
+ });
40
+
41
+ test("the CMS's own editions all have a photographer", () => {
42
+ const works = editionWorks(new URL('..', import.meta.url).pathname);
43
+ assert.ok(works.length > 0);
44
+ for (const work of works) assert.deepEqual(workProblems(work, work.id), []);
45
+ });
package/lib/licenses.mjs CHANGED
@@ -433,6 +433,40 @@ const HELPERS = [
433
433
  ];
434
434
  const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols|theme-coverage)$/, /^\0site-theme-icons?(:|$)/, /^\0app-self:/];
435
435
 
436
+ // ---------------------------------------------------------------------------------------------------
437
+ // The editions' photos
438
+
439
+ /** The licence of the editions' photos (release-notes/editions/), as "Licenser" shows it. */
440
+ export const UNSPLASH_LICENSE = 'Unsplash License';
441
+
442
+ /** The photo of each edition the CMS carries (release-notes/editions/<version>.json and .webp, shown on the start screen
443
+ * after an update) as a work in "Licenser": a picture in the editor, its id `editions/<version>`, named after its place
444
+ * (en-US), by its photographer under the Unsplash License (license-texts/Unsplash.txt). An edition without a photographer
445
+ * gives a work without an author, which workProblems stops the build on.
446
+ * @param {string} cmsRoot the CMS package's folder @returns {object[]} */
447
+ export function editionWorks(cmsRoot) {
448
+ const dir = join(cmsRoot, 'release-notes', 'editions');
449
+ if (!existsSync(dir)) return [];
450
+ const text = read(fileURLToPath(new URL('./license-texts/Unsplash.txt', import.meta.url)));
451
+ return readdirSync(dir)
452
+ .filter((f) => f.endsWith('.json'))
453
+ .sort()
454
+ .map((f) => {
455
+ const version = f.slice(0, -'.json'.length);
456
+ const edition = JSON.parse(readFileSync(join(dir, f), 'utf8'));
457
+ return {
458
+ id: `editions/${version}`,
459
+ name: edition.place?.['en-US'] ?? edition.name,
460
+ version: null,
461
+ author: edition.photo?.by ?? null,
462
+ license: UNSPLASH_LICENSE,
463
+ text,
464
+ kind: 'image',
465
+ where: ['editor'],
466
+ };
467
+ });
468
+ }
469
+
436
470
  /** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
437
471
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
438
472
  * (an app installed; the theme in use, and the standard theme it builds on).
@@ -628,6 +662,12 @@ export function licensesIntegration({ root, iconPacks, owners }) {
628
662
  if (pack.theme) packWorks.set(pack.theme, [...(packWorks.get(pack.theme) ?? []), ...own.map((w) => w.id)]);
629
663
  }
630
664
 
665
+ // The editions' photos, which the editor shows after an update.
666
+ for (const work of editionWorks(fileURLToPath(new URL('..', import.meta.url)))) {
667
+ problems.push(...workProblems(work, `Udgaven ${work.id}`));
668
+ add(work);
669
+ }
670
+
631
671
  // The apps' and the themes' own licences and what they declare (checked by npm run check:content, with what they
632
672
  // give in place of what a package lacks), shipped or not; and what is shipped through them alone.
633
673
  const owned = [];
@@ -66,6 +66,22 @@ export function bundledNotes(files: Record<string, ReleasedNotes>, images: Recor
66
66
  text: string;
67
67
  notes: Record<string, ReleaseNote[]>;
68
68
  }[];
69
+ /** @typedef {{ version: string, name: string, place: Record<string, string>, by: string | null, image: string }} Edition name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address */
70
+ /**
71
+ * The edition the running version belongs to: of the editions in the package (release-notes/editions/<version>.json with
72
+ * its photo <version>.webp), the newest whose version is at most `version` (a test version x.y.z-next-… counts as x.y.z).
73
+ * @param {Record<string, { name?: string, place?: Record<string, string>, photo?: { by?: string } }>} files path -> the edition's file
74
+ * @param {Record<string, string>} images path -> the photo's address
75
+ * @param {string} version the running version
76
+ * @returns {Edition | undefined} undefined when there is none, or it has no name or no photo
77
+ */
78
+ export function editionOf(files: Record<string, {
79
+ name?: string;
80
+ place?: Record<string, string>;
81
+ photo?: {
82
+ by?: string;
83
+ };
84
+ }>, images: Record<string, string>, version: string): Edition | undefined;
69
85
  /**
70
86
  * The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
71
87
  * per locale. Screenshots: one per card, at imagePath(). A missing or unreadable notes file leaves its locale out; a missing
@@ -120,6 +136,16 @@ export const IMAGE_WIDTH: 750;
120
136
  export const MAX_IMAGE_HEIGHT: 800;
121
137
  export function staleImageDirs(dirs: string[], version: string): string[];
122
138
  export function imageKey(version: string, locale: string, id: string): string;
139
+ /**
140
+ * name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address
141
+ */
142
+ export type Edition = {
143
+ version: string;
144
+ name: string;
145
+ place: Record<string, string>;
146
+ by: string | null;
147
+ image: string;
148
+ };
123
149
  export type ReleaseNote = {
124
150
  id: string;
125
151
  kind: "card";
@@ -2,7 +2,7 @@
2
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
4
  // Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor, notesSince, bundledNotes, staleImageDirs, notesFromTar, imageUrl, imageKey,
5
- // parseImagePath and withImageUrls. Depends on ./cms-version.mjs and ./changelog.mjs; no node: imports.
5
+ // parseImagePath, withImageUrls and editionOf. 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';
8
8
 
@@ -208,6 +208,28 @@ export function bundledNotes(files, images, version) {
208
208
  .map((v) => ({ version: v, text: '', notes: byVersion[v] }));
209
209
  }
210
210
 
211
+ /** @typedef {{ version: string, name: string, place: Record<string, string>, by: string | null, image: string }} Edition name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address */
212
+
213
+ /**
214
+ * The edition the running version belongs to: of the editions in the package (release-notes/editions/<version>.json with
215
+ * its photo <version>.webp), the newest whose version is at most `version` (a test version x.y.z-next-… counts as x.y.z).
216
+ * @param {Record<string, { name?: string, place?: Record<string, string>, photo?: { by?: string } }>} files path -> the edition's file
217
+ * @param {Record<string, string>} images path -> the photo's address
218
+ * @param {string} version the running version
219
+ * @returns {Edition | undefined} undefined when there is none, or it has no name or no photo
220
+ */
221
+ export function editionOf(files, images, version) {
222
+ const [base] = String(version ?? '').split('-');
223
+ if (!PLAIN.test(base)) return undefined;
224
+ const editions = Object.entries(files).flatMap(([path, file]) => {
225
+ const v = /\/editions\/([^/]+)\.json$/.exec(path)?.[1];
226
+ if (!v || !PLAIN.test(v) || compareVersions(v, base) > 0 || !isFilled(file?.name)) return [];
227
+ const image = Object.entries(images).find(([imagePath]) => imagePath.endsWith(`/editions/${v}.webp`))?.[1];
228
+ return image ? [{ version: v, name: file.name.trim(), place: file.place ?? {}, by: file.photo?.by ?? null, image }] : [];
229
+ });
230
+ return editions.sort((a, b) => compareVersions(b.version, a.version))[0];
231
+ }
232
+
211
233
  /**
212
234
  * The screenshot folders of older minor lines: each x.y.z name whose major and minor differ from `version`'s. Never
213
235
  * `pending` or any other name.
@@ -19,6 +19,7 @@ import {
19
19
  imageKey,
20
20
  parseImagePath,
21
21
  withImageUrls,
22
+ editionOf,
22
23
  } from './releaseNotes.mjs';
23
24
 
24
25
  const card = (id, overrides = {}) => ({ id, kind: 'card', title: `Title ${id}`, text: `Text ${id}`, ...overrides });
@@ -360,3 +361,40 @@ test('withImageUrls sets the address on cards only and leaves the input alone',
360
361
  });
361
362
 
362
363
  test('withImageUrls gives undefined without notes', () => assert.equal(withImageUrls('0.14.0', undefined), undefined));
364
+
365
+ test('editionOf picks the newest edition up to the running version, with its photo', () => {
366
+ const files = {
367
+ '../release-notes/editions/0.13.0.json': { name: 'Lofoten' },
368
+ '../release-notes/editions/0.14.2.json': {
369
+ name: ' Braies ',
370
+ place: { 'en-US': 'Lago di Braies, Italy' },
371
+ photo: { by: 'A Photographer' },
372
+ },
373
+ '../release-notes/editions/0.15.0.json': { name: 'Atacama' },
374
+ };
375
+ const images = {
376
+ '../release-notes/editions/0.13.0.webp': '/a/lofoten.webp',
377
+ '../release-notes/editions/0.14.2.webp': '/a/braies.webp',
378
+ '../release-notes/editions/0.15.0.webp': '/a/atacama.webp',
379
+ };
380
+ assert.deepEqual(editionOf(files, images, '0.14.2'), {
381
+ version: '0.14.2',
382
+ name: 'Braies',
383
+ place: { 'en-US': 'Lago di Braies, Italy' },
384
+ by: 'A Photographer',
385
+ image: '/a/braies.webp',
386
+ });
387
+ assert.equal(editionOf(files, images, '0.14.5').name, 'Braies');
388
+ assert.equal(editionOf(files, images, '0.14.2-next-20261107125019').name, 'Braies');
389
+ assert.equal(editionOf(files, images, '0.14.1').name, 'Lofoten');
390
+ assert.deepEqual(editionOf(files, images, '0.14.1').place, {});
391
+ assert.equal(editionOf(files, images, '0.14.1').by, null);
392
+ assert.equal(editionOf(files, images, '0.12.9'), undefined);
393
+ assert.equal(editionOf(files, images, 'not a version'), undefined);
394
+ });
395
+
396
+ test('editionOf skips an edition without a name or a photo', () => {
397
+ const files = { '../release-notes/editions/0.14.0.json': { name: 'Braies' }, '../release-notes/editions/0.14.1.json': { name: '' } };
398
+ assert.equal(editionOf(files, {}, '0.14.2'), undefined);
399
+ assert.equal(editionOf(files, { '../release-notes/editions/0.14.1.webp': '/x.webp' }, '0.14.2'), undefined);
400
+ });
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-20261107145215",
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
+ }
@@ -4,46 +4,46 @@
4
4
  {
5
5
  "id": "apps-turn-off",
6
6
  "kind": "card",
7
- "title": "Slå en app fra uden at miste den",
8
- "text": "Nogle gange vil du sætte en app på pause uden at skulle sætte den op igen bagefter. Åbn appen under Apps, og slå Aktiveret fra lige under beskrivelsen. Indstillingerne bliver liggende, så du kan slå den til igen, når du vil."
7
+ "title": "Sæt en app på pause uden at miste indstillingerne",
8
+ "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
9
  },
10
10
  {
11
11
  "id": "developer-menu",
12
12
  "kind": "line",
13
- "text": "Avanceret hedder nu Udvikler under Indstillinger, med Dele samt Caching og builds; gamle links virker stadig."
13
+ "text": "Avanceret under Indstillinger hedder nu Udvikler, og dine gamle links virker stadig"
14
14
  },
15
15
  {
16
16
  "id": "dynamic-lists-page",
17
17
  "kind": "line",
18
- "text": "Lister hedder nu Dynamiske lister, én række i menuen, der åbner en side med alle dine lister."
18
+ "text": "Lister i menuen hedder nu Dynamiske lister og åbner en side med alle dine lister"
19
19
  },
20
20
  {
21
21
  "id": "editor-footer",
22
22
  "kind": "card",
23
- "title": "Se om GitHub og Cloudflare virker",
24
- "text": "Når noget ikke vil gemme eller udgive, vil du gerne vide, om problemet er din hjemmeside eller de tjenester, den kører på. Rul ned til bunden af editoren, og tryk på Driftsstatus. Den tjekker kun de tjenester, din hjemmeside bruger."
23
+ "title": "Tjek GitHub og Cloudflare",
24
+ "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
25
  },
26
26
  {
27
27
  "id": "editor-toolbar-dock",
28
28
  "kind": "line",
29
- "text": "På en telefon er værktøjslinjen til tekst slankere og holder samme afstand til kanterne."
29
+ "text": "På en telefon fylder værktøjslinjen til tekst mindre"
30
30
  },
31
31
  {
32
32
  "id": "google-preview-closer",
33
33
  "kind": "card",
34
- "title": "En Google-forhåndsvisning, der ligner Google",
35
- "text": "Du vil gerne se, hvordan en side ser ud på Google, før du udgiver den. Åbn en side eller et indlæg, og se Google-forhåndsvisningen. Den ligner nu Google på både telefon og computer, og dit vigtigste søgeord står med fed."
34
+ "title": "Se din side, som Google viser den",
35
+ "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
36
  },
37
37
  {
38
38
  "id": "morpheme-connection-page",
39
39
  "kind": "line",
40
- "text": "Din Forbindelse til Appstore ligger nu på siden Morpheme under Indstillinger."
40
+ "text": "Forbindelse til Appstore ligger nu på siden Morpheme under Indstillinger"
41
41
  },
42
42
  {
43
43
  "id": "quiet-lamps-wander",
44
44
  "kind": "card",
45
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."
46
+ "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
47
  }
48
48
  ]
49
49
  }
@@ -4,12 +4,12 @@
4
4
  {
5
5
  "id": "drawer-back-changes-screen",
6
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."
7
+ "text": "Swiper du tilbage på en telefon, mens menuen er åben, lukker menuen, og du kommer til skærmen før"
8
8
  },
9
9
  {
10
10
  "id": "notes-from-package",
11
11
  "kind": "line",
12
- "text": "Noterne til din version vises nu lige efter en opdatering."
12
+ "text": "Noter som disse vises nu, lige når du har opdateret"
13
13
  }
14
14
  ]
15
15
  }
@@ -4,18 +4,18 @@
4
4
  {
5
5
  "id": "footer-scrolls-above-fab",
6
6
  "kind": "line",
7
- "text": "På en telefon kan du nu rulle editorens bundlinje fri af plusknappen, også på korte skærme."
7
+ "text": "På en telefon skjuler plusknappen ikke længere linkene nederst i editoren"
8
8
  },
9
9
  {
10
10
  "id": "lists-follow-md3",
11
11
  "kind": "line",
12
- "text": "Lister i editoren viser nu lyse rækker på en tonet side, som i Material 3."
12
+ "text": "Lister i editoren har større tekst og skiller sig bedre ud fra siden"
13
13
  },
14
14
  {
15
15
  "id": "update-card-whats-new",
16
16
  "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."
17
+ "title": "Nyhederne står på din startskærm",
18
+ "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
19
  }
20
20
  ]
21
21
  }
@@ -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
+ }
@@ -4,46 +4,46 @@
4
4
  {
5
5
  "id": "apps-turn-off",
6
6
  "kind": "card",
7
- "title": "Turn an app off without losing it",
8
- "text": "Sometimes you want to pause an app without setting it up again later. Open the app under Apps and turn off Enabled, right below its description. Its settings stay, so you can turn it on again whenever you like."
7
+ "title": "Pause an app without losing its settings",
8
+ "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
9
  },
10
10
  {
11
11
  "id": "developer-menu",
12
12
  "kind": "line",
13
- "text": "Advanced is now Developer under Settings, with Parts and Caching and builds; old links still work."
13
+ "text": "Advanced under Settings is now called Developer, and your old links still work"
14
14
  },
15
15
  {
16
16
  "id": "dynamic-lists-page",
17
17
  "kind": "line",
18
- "text": "Lists is now Dynamic lists, one menu row that opens a page with all your lists."
18
+ "text": "Lists in the menu is now Dynamic lists and opens a page with all your lists"
19
19
  },
20
20
  {
21
21
  "id": "editor-footer",
22
22
  "kind": "card",
23
- "title": "See if GitHub and Cloudflare are working",
24
- "text": "When something won't save or publish, you want to know if the problem is your site or the services it runs on. Scroll to the bottom of the editor and tap System status. It checks only the services your site uses."
23
+ "title": "Check GitHub and Cloudflare",
24
+ "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
25
  },
26
26
  {
27
27
  "id": "editor-toolbar-dock",
28
28
  "kind": "line",
29
- "text": "On a phone, the text toolbar is slimmer and keeps an even gap to the edges."
29
+ "text": "On a phone, the text toolbar takes up less room"
30
30
  },
31
31
  {
32
32
  "id": "google-preview-closer",
33
33
  "kind": "card",
34
- "title": "A Google preview that looks like Google",
35
- "text": "You want to see how a page looks in Google before you publish it. Open a page or post and look at its Google preview. It now matches Google on a phone and a computer, with your main keyword in bold."
34
+ "title": "See your page as Google shows it",
35
+ "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
36
  },
37
37
  {
38
38
  "id": "morpheme-connection-page",
39
39
  "kind": "line",
40
- "text": "Your Store connection now sits on the Morpheme page under Settings."
40
+ "text": "Store connection is now on the Morpheme page under Settings"
41
41
  },
42
42
  {
43
43
  "id": "quiet-lamps-wander",
44
44
  "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."
45
+ "title": "See what’s new",
46
+ "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
47
  }
48
48
  ]
49
49
  }
@@ -4,12 +4,12 @@
4
4
  {
5
5
  "id": "drawer-back-changes-screen",
6
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."
7
+ "text": "On a phone, swiping back with the menu open closes the menu and takes you to the previous screen"
8
8
  },
9
9
  {
10
10
  "id": "notes-from-package",
11
11
  "kind": "line",
12
- "text": "The notes for your version now show right after an update."
12
+ "text": "Notes like these now show right after you update"
13
13
  }
14
14
  ]
15
15
  }
@@ -4,18 +4,18 @@
4
4
  {
5
5
  "id": "footer-scrolls-above-fab",
6
6
  "kind": "line",
7
- "text": "On a phone, you can now scroll the editor's footer clear of the add button, also on short screens."
7
+ "text": "On a phone, the add button no longer hides the links at the bottom of the editor"
8
8
  },
9
9
  {
10
10
  "id": "lists-follow-md3",
11
11
  "kind": "line",
12
- "text": "Lists in the editor now show light rows on a tinted page, as in Material 3."
12
+ "text": "Lists in the editor have larger text and stand out more from the page"
13
13
  },
14
14
  {
15
15
  "id": "update-card-whats-new",
16
16
  "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."
17
+ "title": "What’s new, right on your start screen",
18
+ "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
19
  }
20
20
  ]
21
21
  }
@@ -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
+ }
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/<version>, 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>}
@@ -0,0 +1,17 @@
1
+ // The smaller changes of a version as a list under "Also in this version", one line each.
2
+ // Exposes ReleaseNoteLines. Depends on i18n and the release note type of lib/releaseNotes.mjs.
3
+ import { t } from './i18n';
4
+ import type { ReleaseNote } from '../lib/releaseNotes.mjs';
5
+
6
+ export function ReleaseNoteLines({ lines }: { lines: ReleaseNote[] }) {
7
+ return (
8
+ <div className="space-y-2">
9
+ <h4 className="text-base font-medium">{t.cms.alsoInThisVersion}</h4>
10
+ <ul className="list-disc space-y-1 ps-5 text-base marker:text-accent">
11
+ {lines.map((note) => (
12
+ <li key={note.id}>{note.text}</li>
13
+ ))}
14
+ </ul>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,12 @@
1
+ // One release note card as a section of the release notes: its title and its text.
2
+ // Exposes ReleaseNoteSection. Depends on the release note type of lib/releaseNotes.mjs.
3
+ import type { ReleaseNote } from '../lib/releaseNotes.mjs';
4
+
5
+ export function ReleaseNoteSection({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
6
+ return (
7
+ <div className="space-y-1">
8
+ <h4 className="text-lg font-medium">{note.title}</h4>
9
+ <p className="text-base">{note.text}</p>
10
+ </div>
11
+ );
12
+ }
@@ -1,11 +1,13 @@
1
1
  // The release notes on the Morpheme page (#morpheme), as an article: its title, then per version a small heading, its
2
2
  // cards in the editor's language (else English) as sections with a title and a text, and its other changes as a list
3
3
  // under "Also in this version"; for a version without notes, its text. No cards and no screenshots: the start screen's
4
- // update card (UpdatesNotice.tsx) shows the pictures. Exposes ReleaseNotes and VersionNotes. Depends on
5
- // lib/releaseNotes.mjs (notesFor) and i18n.
4
+ // update card (UpdatesNotice.tsx) shows the pictures. Exposes ReleaseNotes and VersionNotes; it only composes
5
+ // ReleaseNoteSection.tsx and ReleaseNoteLines.tsx. Depends on lib/releaseNotes.mjs (notesFor) and i18n.
6
6
  import { locale, t } from './i18n';
7
7
  import { notesFor } from '../lib/releaseNotes.mjs';
8
8
  import type { ReleaseNote } from '../lib/releaseNotes.mjs';
9
+ import { ReleaseNoteSection } from './ReleaseNoteSection';
10
+ import { ReleaseNoteLines } from './ReleaseNoteLines';
9
11
 
10
12
  export type VersionNotes = { version: string; text: string; notes?: Record<string, ReleaseNote[]> };
11
13
 
@@ -20,21 +22,9 @@ function VersionEntry({ entry }: { entry: VersionNotes }) {
20
22
  <h3 className="text-sm font-medium text-muted">{t.cms.running(entry.version)}</h3>
21
23
  {!notes.length && entry.text && <p className="text-base">{entry.text}</p>}
22
24
  {cards.map((note) => (
23
- <div key={note.id} className="space-y-1">
24
- <h4 className="text-lg font-medium">{note.title}</h4>
25
- <p className="text-base">{note.text}</p>
26
- </div>
25
+ <ReleaseNoteSection key={note.id} note={note} />
27
26
  ))}
28
- {lines.length > 0 && (
29
- <div className="space-y-2">
30
- <h4 className="text-base font-medium">{t.cms.alsoInThisVersion}</h4>
31
- <ul className="list-disc space-y-1 ps-5 text-base marker:text-accent">
32
- {lines.map((note) => (
33
- <li key={note.id}>{note.text}</li>
34
- ))}
35
- </ul>
36
- </div>
37
- )}
27
+ {lines.length > 0 && <ReleaseNoteLines lines={lines} />}
38
28
  </section>
39
29
  );
40
30
  }
package/src/Screen.tsx CHANGED
@@ -14,7 +14,7 @@
14
14
  // Under the content of every screen is the editor's footer (EditorFooter.tsx), at the bottom of the window when the content
15
15
  // is short (the screen fills the column under the bar, TopBarLayout). Below medium, while the screen has a floating action
16
16
  // button (fabPresence.tsx), there is room for it under the footer (data-clearance): the page is that much taller than the
17
- // window (--editor-fab-clearance, editor.css), so a short screen still has its footer at the bottom of the window and can
17
+ // window (--fab-room and --editor-fab-clearance, editor.css), so a short screen still has its footer at the bottom of the window and can
18
18
  // be scrolled up to show it above the FAB.
19
19
  import { useId, type MouseEvent, type ReactNode } from 'react';
20
20
  import { createPortal } from 'react-dom';
@@ -180,7 +180,7 @@ export default function Screen<T extends string>({
180
180
  <div className="flex flex-1">
181
181
  <main
182
182
  data-clearance={fabs.count > 0}
183
- className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[env(safe-area-inset-bottom)] data-[clearance=true]:pb-[calc(4.5rem+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 medium:pb-4 medium:data-[clearance=true]:pb-4 ${width}`}
183
+ className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[env(safe-area-inset-bottom)] data-[clearance=true]:pb-[calc(var(--fab-room)+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 medium:pb-4 medium:data-[clearance=true]:pb-4 ${width}`}
184
184
  >
185
185
  {/* At expanded and wider: the way here, the title and the status, under the bar. */}
186
186
  <div className="mb-6 hidden min-w-0 items-center gap-x-2 expanded:flex">
@@ -1,11 +1,13 @@
1
- // At the top of the start screen: what was updated since the last visit in this browser, as a card with a close button
2
- // that marks it seen, in the accent (the editor's blue). For the CMS: its new version large, how many new things it brings and the first of them from the
3
- // notes the package carries (bundledNotes.ts: the first with a screenshot large, then the titles and texts of the next),
4
- // and a button to the Morpheme page with all of them; for each app installed from the store: a row with its icon and new
5
- // version, to its Information tab. Exposes UpdatesNotice. The logic is startView.mjs's (changesSince) and
6
- // releaseNotes.mjs's (notesSince), the storage startStorage.ts's, the card, rows and buttons the theme's (themeUi.ts, ui.tsx).
7
- import { useEffect, useState } from 'react';
8
- import { ArrowRight, Sparkle } from '@phosphor-icons/react';
1
+ // At the top of the start screen: what was updated since the last visit in this browser, as a card with Close, which
2
+ // marks it seen, and Read more. For the CMS: the photo of its edition (release-notes/editions/, named after the place the
3
+ // photo is from), 3:2 with rounded corners, above the text in the compact window class and from the medium class up to
4
+ // the left of it, as high as the card; "Morpheme <edition> <version>" (the headline), the place in full (the subhead,
5
+ // with an info button after it that credits the photographer, PhotoCredit), how many new things it brings (the supporting
6
+ // text, bundledNotes.ts) and Read more to the Morpheme page with all of them; for each app installed from the store: a
7
+ // row with its icon and new version, to its Information tab. Exposes UpdatesNotice. The logic is startView.mjs's
8
+ // (changesSince) and releaseNotes.mjs's (notesSince, editionOf), the storage startStorage.ts's, the card, rows, buttons
9
+ // and tooltip the theme's (themeUi.ts, ui.tsx).
10
+ import { useEffect, useRef, useState } from 'react';
9
11
  import { version } from '../package.json';
10
12
  import { locale, t } from './i18n';
11
13
  import { installedApps } from './apps';
@@ -13,17 +15,16 @@ import { apps } from './site';
13
15
  import { changesSince, versioned, type Versions } from './startView.mjs';
14
16
  import { keepStored, SEEN, storedSeen } from './startStorage';
15
17
  import type { RepoFile } from './github';
16
- import { SiteCard } from './themeUi';
17
- import { CloseButton, LinkButton, ListItem, plainListClass } from './ui';
18
+ import { SiteCard, SiteTooltip } from './themeUi';
19
+ import { Svg } from './icon';
20
+ import { Button, LinkButton, ListItem, plainListClass } from './ui';
18
21
  import { CMS_ROUTE } from './CmsPage';
22
+ import { LICENSES_ROUTE } from './Licenses';
19
23
  import { appHref } from './AppList';
20
- import { runningNotes } from './bundledNotes';
21
- import { notesSince, type ReleaseNote } from '../lib/releaseNotes.mjs';
24
+ import { runningEdition, runningNotes } from './bundledNotes';
25
+ import { notesSince } from '../lib/releaseNotes.mjs';
22
26
  import { INFO_KEY } from '../lib/appRoute.mjs';
23
27
 
24
- /** How many of the new things the card shows; the Morpheme page has them all. */
25
- const SHOWN = 3;
26
-
27
28
  /** The CMS's version and those of the apps installed from the store, as the editor is built with them now. */
28
29
  function versionsNow(files: RepoFile[]): Versions {
29
30
  const installed = installedApps(files);
@@ -33,28 +34,61 @@ function versionsNow(files: RepoFile[]): Versions {
33
34
  };
34
35
  }
35
36
 
36
- /** The first new thing with a screenshot, large: the screenshot fills the top (its top part), its title under it. */
37
- function Lead({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
37
+ /** The info button right after the place's name (the theme's info icon, which every icon pack has), with a rich
38
+ * tooltip: who took the photo, on Unsplash, and a link to its licence under "Licenses". It opens while the mouse is over
39
+ * the button or the tooltip, while focus is in them, and on a tap (a second tap closes it); Escape closes it. Under the
40
+ * button, from its end, at most 15rem wide, so it stays on a phone's screen. */
41
+ function PhotoCredit({ by, version }: { by: string; version: string }) {
42
+ const [open, setOpen] = useState(false);
43
+ const leave = useRef<ReturnType<typeof setTimeout>>(undefined);
44
+ // When it opened: a tap focuses the button (which opens it) just before its click, which must not close it again.
45
+ const openedAt = useRef(0);
46
+ const show = () => {
47
+ clearTimeout(leave.current);
48
+ if (!open) openedAt.current = Date.now();
49
+ setOpen(true);
50
+ };
51
+ const hide = () => {
52
+ clearTimeout(leave.current);
53
+ leave.current = setTimeout(() => setOpen(false), 150);
54
+ };
55
+ // Escape closes it wherever the pointer and the focus are (WCAG 1.4.13).
56
+ useEffect(() => {
57
+ if (!open) return;
58
+ const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false);
59
+ document.addEventListener('keydown', onKey);
60
+ return () => document.removeEventListener('keydown', onKey);
61
+ }, [open]);
62
+ useEffect(() => () => clearTimeout(leave.current), []);
38
63
  return (
39
- <div className="overflow-hidden rounded-xl bg-accent-container text-on-accent-container">
40
- <img src={note.image} alt="" className="block h-44 w-full object-cover object-top" />
41
- <p className="px-4 py-3 font-medium">{note.title}</p>
64
+ <div
65
+ className="-my-3 -ms-2.5 inline-flex align-middle"
66
+ onMouseEnter={show}
67
+ onMouseLeave={hide}
68
+ onFocus={show}
69
+ onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && hide()}
70
+ >
71
+ <SiteTooltip
72
+ open={open}
73
+ // Only `open` shows it: not the theme's fallback on hover and focus, which Escape could not close.
74
+ className="[&:not([data-tooltip-open])>.tooltip]:invisible [&:not([data-tooltip-open])>.tooltip]:opacity-0 [&>.tooltip]:start-auto [&>.tooltip]:end-0 [&>.tooltip]:top-[calc(100%-0.5rem)] [&>.tooltip]:bottom-auto [&>.tooltip]:max-w-60"
75
+ supportingText={t.library.photoBy(by)}
76
+ action={{ label: t.library.photoLicense, href: `#${LICENSES_ROUTE}/${encodeURIComponent(`editions/${version}`)}` }}
77
+ >
78
+ <button
79
+ type="button"
80
+ aria-label={t.library.photoInfo}
81
+ aria-expanded={open}
82
+ onClick={() => (open && Date.now() - openedAt.current < 400 ? undefined : open ? setOpen(false) : show())}
83
+ className="grid size-12 place-items-center rounded-full text-muted focus-visible:outline-2 focus-visible:outline-offset-[-8px]"
84
+ >
85
+ <Svg name="info" size={20} />
86
+ </button>
87
+ </SiteTooltip>
42
88
  </div>
43
89
  );
44
90
  }
45
91
 
46
- /** One more new thing: a sparkle and its title (a card) or its text (a line). */
47
- function Highlight({ note }: { note: ReleaseNote }) {
48
- return (
49
- <li className="flex items-center gap-3 rounded-xl bg-accent-container p-3 text-on-accent-container">
50
- <span className="grid size-10 shrink-0 place-items-center rounded-full bg-accent text-on-accent">
51
- <Sparkle size={20} weight="fill" aria-hidden />
52
- </span>
53
- <span className={note.kind === 'card' ? 'font-medium' : 'text-sm'}>{note.kind === 'card' ? note.title : note.text}</span>
54
- </li>
55
- );
56
- }
57
-
58
92
  /** The first visit, and a visit with nothing new, only notes the versions. */
59
93
  export function UpdatesNotice({ files }: { files: RepoFile[] }) {
60
94
  const now = versionsNow(files);
@@ -74,48 +108,49 @@ export function UpdatesNotice({ files }: { files: RepoFile[] }) {
74
108
  return app ? [{ app, version: c.version }] : [];
75
109
  });
76
110
  const news = cms ? notesSince(runningNotes, cms.from, locale) : [];
77
- const lead = news.find((note): note is Extract<ReleaseNote, { kind: 'card' }> => note.kind === 'card' && !!note.image);
78
- const rest = news.filter((note) => note !== lead);
111
+ const edition = cms && runningEdition;
112
+ const place = edition && (edition.place[locale] ?? edition.place['en-US']);
113
+ const title = cms
114
+ ? edition
115
+ ? t.library.cmsEdition(edition.name, cms.version)
116
+ : t.library.cmsVersion(cms.version)
117
+ : t.library.appsUpdated(updatedApps.length);
79
118
  return (
80
- <SiteCard
81
- kind="filled"
82
- as="section"
83
- role="status"
84
- aria-labelledby="updates-title"
85
- className="relative mb-8 text-on-accent [--card-filled-surface:var(--accent)] [--card-padding:0] [--icon-button-standard-text:var(--on-accent)]"
86
- >
87
- <CloseButton onClick={close} />
88
- <div className="flex items-center gap-4 p-4 pe-14">
89
- <span className="grid size-12 shrink-0 place-items-center rounded-full bg-on-accent text-accent">
90
- <Sparkle size={26} weight="fill" aria-hidden />
91
- </span>
92
- <div className="min-w-0">
93
- <h2 id="updates-title" className="text-sm font-medium">
94
- {t.library.updatesTitle}
119
+ <SiteCard kind="outlined" as="section" role="status" aria-labelledby="updates-title" className="mb-8 [--card-padding:0]">
120
+ <div className="grid medium:grid-cols-[auto_1fr]">
121
+ {edition && (
122
+ <div className="relative aspect-[3/2] w-full medium:h-full medium:w-auto">
123
+ <img src={edition.image} alt="" className="absolute inset-0 size-full rounded-xl object-cover" />
124
+ </div>
125
+ )}
126
+ <div className="flex min-w-0 flex-col p-4">
127
+ <h2
128
+ id="updates-title"
129
+ className="text-[length:var(--type-headline-small-size)] leading-[var(--type-headline-small-line-height)] [overflow-wrap:anywhere]"
130
+ >
131
+ {title}
95
132
  </h2>
96
- <p className="text-2xl leading-tight">{cms ? t.library.cmsVersion(cms.version) : t.library.appsUpdated(updatedApps.length)}</p>
97
- {news.length > 0 && <p className="text-sm opacity-85">{t.library.newThings(news.length)}</p>}
133
+ {place && (
134
+ <p className="mt-1 text-sm">
135
+ {place}
136
+ {edition.by && <PhotoCredit by={edition.by} version={edition.version} />}
137
+ </p>
138
+ )}
139
+ {news.length > 0 && <p className="mt-2 text-sm text-muted">{t.library.newThings(news.length)}</p>}
140
+ <div className="mt-auto flex flex-wrap justify-end gap-2 pt-4">
141
+ <Button kind="text" size="small" onClick={close}>
142
+ {t.common.close}
143
+ </Button>
144
+ {cms && (
145
+ <LinkButton kind="filled" size="small" href={`#${CMS_ROUTE}`}>
146
+ {t.library.readMore}
147
+ </LinkButton>
148
+ )}
149
+ </div>
98
150
  </div>
99
151
  </div>
100
- {news.length > 0 && (
101
- <div className="space-y-2 px-4">
102
- {lead && <Lead note={lead} />}
103
- <ul className="space-y-2">
104
- {rest.slice(0, lead ? SHOWN - 1 : SHOWN).map((note) => (
105
- <Highlight key={note.id} note={note} />
106
- ))}
107
- </ul>
108
- </div>
109
- )}
110
- {cms && (
111
- <div className="px-4 pt-4 pb-4">
112
- <LinkButton kind="tonal" href={`#${CMS_ROUTE}`} icon={ArrowRight}>
113
- {t.library.seeWhatsNew}
114
- </LinkButton>
115
- </div>
116
- )}
117
152
  {updatedApps.length > 0 && (
118
- <ul className={`${plainListClass} mx-4 mb-4 overflow-hidden rounded-xl bg-accent-container text-on-accent-container`}>
153
+ <ul className={`${plainListClass} border-t border-line`}>
119
154
  {updatedApps.map(({ app, version }) => (
120
155
  <ListItem
121
156
  key={app.id}
@@ -1,7 +1,9 @@
1
1
  // The release notes of the minor line this package is in (x.y.0 up to its version): read at build time from the package's own release-notes/ files.
2
- // Exposes runningNotes (the entries the Morpheme page shows, newest first; empty when the line has none).
2
+ // Exposes runningNotes (the entries the Morpheme page shows, newest first; empty when the line has none) and runningEdition
3
+ // (the edition this version belongs to: the place its photo is from, short and in full, and the photo; undefined when there is none)
4
+ // and editionPhotos (each edition's photo by its version).
3
5
  // Depends on lib/releaseNotes.mjs (bundledNotes) and package.json's version. The globs are relative, so they work here and in a site's node_modules.
4
- import { bundledNotes } from '../lib/releaseNotes.mjs';
6
+ import { bundledNotes, editionOf } from '../lib/releaseNotes.mjs';
5
7
  import { version } from '../package.json';
6
8
  import type { ReleasedNotes } from '../lib/releaseNotes.mjs';
7
9
 
@@ -9,3 +11,25 @@ const files = import.meta.glob<ReleasedNotes>('../release-notes/i18n/*/*.json',
9
11
  const images = import.meta.glob<string>('../release-notes/i18n/*/*/*.webp', { eager: true, query: '?url', import: 'default' });
10
12
 
11
13
  export const runningNotes = bundledNotes(files, images, version);
14
+
15
+ const editions = import.meta.glob<{ name?: string; place?: Record<string, string>; photo?: { by?: string } }>(
16
+ '../release-notes/editions/*.json',
17
+ {
18
+ eager: true,
19
+ import: 'default',
20
+ },
21
+ );
22
+ const editionImages = import.meta.glob<string>('../release-notes/editions/*.webp', { eager: true, query: '?url', import: 'default' });
23
+
24
+ export const runningEdition = editionOf(editions, editionImages, version);
25
+
26
+ /** Each edition's photo, whole, by its version: on its page under "Licenses" (LicensePage). */
27
+ export const editionPhotos: Record<string, string> = Object.fromEntries(
28
+ Object.entries(editionImages).map(([path, url]) => [
29
+ path
30
+ .split('/')
31
+ .pop()!
32
+ .replace(/\.webp$/, ''),
33
+ url,
34
+ ]),
35
+ );
package/src/editor.css CHANGED
@@ -8,6 +8,9 @@
8
8
  --editor-menu-surface: var(--surface-container-lowest);
9
9
  --editor-menu-shadow: var(--elevation-2);
10
10
 
11
+ /* The room a screen leaves for its FAB under the footer: Screen.tsx's padding and --editor-fab-clearance (below medium). */
12
+ --fab-room: 4.5rem;
13
+
11
14
  /* How a check went (CheckList: the SEO rating), as PageSpeed shows it: the mark before a check that is fine and one
12
15
  that can be done better, and the score's ring (from 90 good, from 50 average, else bad) with its number and the
13
16
  light circle in it; the same in every theme. */
@@ -424,7 +427,7 @@
424
427
  the window and can be scrolled up to show it above the FAB. */
425
428
  @media (width < theme(--breakpoint-medium)) {
426
429
  :root:has(.editor-screen[data-clearance='true']) {
427
- --editor-fab-clearance: 4.5rem;
430
+ --editor-fab-clearance: var(--fab-room);
428
431
  }
429
432
  }
430
433
 
@@ -8,10 +8,13 @@ export const library: Messages['library'] = {
8
8
  continueTitle: 'Fortsæt, hvor du slap',
9
9
  continueEmpty: 'De sider og indlæg, du redigerer, vises her.',
10
10
  recentEdit: (kind, when) => `${kind} · Redigeret ${when}`,
11
- updatesTitle: 'Opdateret siden dit sidste besøg',
11
+ cmsEdition: (edition, version) => `Morpheme ${edition} ${version}`,
12
12
  appsUpdated: (count) => (count === 1 ? '1 app er opdateret' : `${count} apps er opdateret`),
13
13
  newThings: (count) => (count === 1 ? '1 nyhed' : `${count} nyheder`),
14
- seeWhatsNew: 'Se, hvad der er nyt',
14
+ readMore: 'Læs mere',
15
+ photoInfo: 'Om billedet',
16
+ photoBy: (name) => `Foto af ${name} på Unsplash`,
17
+ photoLicense: 'Se licens',
15
18
  appUpdatedTo: (version) => `Opdateret til ${version}`,
16
19
  githubAccount: (login) => `GitHub-konto: ${login}`,
17
20
  logOut: 'Log ud',
@@ -15,13 +15,19 @@ export const library = {
15
15
  /** Under each of them: its kind ("Page") and when it was saved ("today at 1:42 PM", when.ts). */
16
16
  recentEdit: (kind: string, when: string) => `${kind} · Edited ${when}`,
17
17
  /** At the top of the start screen (UpdatesNotice.tsx): the card with what was updated since the last visit in this
18
- * browser. Its title, over the CMS's new version (cmsVersion) or, with only apps updated, how many. */
19
- updatesTitle: 'Updated since your last visit',
18
+ * browser. Its headline: the CMS's name, its edition (the place the edition's photo is from: "Braies") and its new
19
+ * version; without an edition, cmsVersion; with only apps updated, how many (appsUpdated). */
20
+ cmsEdition: (edition: string, version: string) => `Morpheme ${edition} ${version}`,
20
21
  appsUpdated: (count: number) => (count === 1 ? '1 app updated' : `${count} apps updated`),
21
22
  /** Under the CMS's new version: how many new things its release notes have since the version seen last. */
22
23
  newThings: (count: number) => (count === 1 ? '1 new thing' : `${count} new things`),
23
24
  /** The button to the Morpheme page, with all the release notes. */
24
- seeWhatsNew: 'See what’s new',
25
+ readMore: 'Read more',
26
+ /** The info button on the edition's photo in that card, and its tooltip: who took the photo, and a link to its
27
+ * licence under "Licenses". */
28
+ photoInfo: 'About the photo',
29
+ photoBy: (name: string) => `Photo by ${name} on Unsplash`,
30
+ photoLicense: 'See license',
25
31
  /** Under an updated app's name. */
26
32
  appUpdatedTo: (version: string) => `Updated to ${version}`,
27
33
  githubAccount: (login: string) => `GitHub account: ${login}`,