@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 +7 -0
- package/lib/license-texts/Unsplash.txt +7 -0
- package/lib/licenses.d.mts +8 -0
- package/lib/licenses.editions.test.mjs +45 -0
- package/lib/licenses.mjs +40 -0
- package/lib/releaseNotes.d.mts +26 -0
- package/lib/releaseNotes.mjs +23 -1
- package/lib/releaseNotes.test.mjs +38 -0
- package/package.json +1 -1
- package/release-notes/editions/0.14.2.json +12 -0
- package/release-notes/editions/0.14.2.webp +0 -0
- package/release-notes/i18n/da-DK/0.14.0.json +11 -11
- package/release-notes/i18n/da-DK/0.14.1.json +2 -2
- package/release-notes/i18n/da-DK/0.14.2.json +4 -4
- package/release-notes/i18n/da-DK/pending/edition-photo-card.json +6 -0
- package/release-notes/i18n/da-DK/pending/edition-photo-card.webp +0 -0
- package/release-notes/i18n/en-US/0.14.0.json +12 -12
- package/release-notes/i18n/en-US/0.14.1.json +2 -2
- package/release-notes/i18n/en-US/0.14.2.json +4 -4
- package/release-notes/i18n/en-US/pending/edition-photo-card.json +6 -0
- package/release-notes/i18n/en-US/pending/edition-photo-card.webp +0 -0
- package/src/Licenses.tsx +4 -0
- package/src/ReleaseNoteLines.tsx +17 -0
- package/src/ReleaseNoteSection.tsx +12 -0
- package/src/ReleaseNotes.tsx +6 -16
- package/src/Screen.tsx +2 -2
- package/src/UpdatesNotice.tsx +104 -69
- package/src/bundledNotes.ts +26 -2
- package/src/editor.css +4 -1
- package/src/i18n/da-DK/library.ts +5 -2
- package/src/i18n/en-US/library.ts +9 -3
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
|
package/lib/licenses.d.mts
CHANGED
|
@@ -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 = [];
|
package/lib/releaseNotes.d.mts
CHANGED
|
@@ -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";
|
package/lib/releaseNotes.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
+
}
|
|
Binary file
|
|
@@ -4,46 +4,46 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": "apps-turn-off",
|
|
6
6
|
"kind": "card",
|
|
7
|
-
"title": "
|
|
8
|
-
"text": "
|
|
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
|
|
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
|
|
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": "
|
|
24
|
-
"text": "
|
|
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
|
|
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": "
|
|
35
|
-
"text": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
18
|
-
"text": "Efter en opdatering
|
|
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
|
}
|
|
Binary file
|
|
@@ -4,46 +4,46 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": "apps-turn-off",
|
|
6
6
|
"kind": "card",
|
|
7
|
-
"title": "
|
|
8
|
-
"text": "
|
|
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
|
|
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
|
|
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": "
|
|
24
|
-
"text": "
|
|
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
|
|
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": "
|
|
35
|
-
"text": "
|
|
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": "
|
|
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
|
|
46
|
-
"text": "
|
|
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
|
|
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": "
|
|
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,
|
|
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
|
|
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
|
|
18
|
-
"text": "After an update,
|
|
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
|
}
|
|
Binary file
|
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
|
+
}
|
package/src/ReleaseNotes.tsx
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
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(
|
|
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">
|
package/src/UpdatesNotice.tsx
CHANGED
|
@@ -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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
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
|
|
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
|
|
37
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
78
|
-
const
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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}
|
|
153
|
+
<ul className={`${plainListClass} border-t border-line`}>
|
|
119
154
|
{updatedApps.map(({ app, version }) => (
|
|
120
155
|
<ListItem
|
|
121
156
|
key={app.id}
|
package/src/bundledNotes.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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}`,
|