@morphemecms/cms 0.13.0 → 0.14.0-next-20261107093528
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 +18 -0
- package/defaults/editor.ts +3 -14
- package/icons/lucide/dots-three-vertical.svg +1 -0
- package/icons/material-symbols/dots-three-vertical.svg +1 -0
- package/icons/phosphor/dots-three-vertical.svg +1 -0
- package/lib/appChanges.d.mts +16 -0
- package/lib/appChanges.mjs +20 -0
- package/lib/appChanges.test.mjs +30 -0
- package/lib/apps.d.mts +2 -2
- package/lib/apps.mjs +13 -7
- package/lib/appsFile.d.mts +20 -0
- package/lib/appsFile.mjs +57 -0
- package/lib/appsFile.test.mjs +53 -0
- package/lib/build-settings.mjs +1 -1
- package/lib/changelog.d.mts +7 -0
- package/lib/changelog.mjs +16 -5
- package/lib/footerClearance.d.mts +1 -0
- package/lib/footerClearance.mjs +9 -0
- package/lib/footerClearance.test.mjs +24 -0
- package/lib/footerLinks.d.mts +12 -0
- package/lib/footerLinks.mjs +26 -0
- package/lib/footerLinks.test.mjs +32 -0
- package/lib/google-result.test.mjs +40 -9
- package/lib/menu.mjs +1 -1
- package/lib/menu.test.mjs +9 -8
- package/lib/releaseNotes.d.mts +136 -0
- package/lib/releaseNotes.mjs +249 -0
- package/lib/releaseNotes.test.mjs +267 -0
- package/lib/serviceFeed.d.mts +2 -0
- package/lib/serviceFeed.mjs +13 -0
- package/lib/serviceFeed.test.mjs +53 -0
- package/lib/serviceStatus.d.mts +72 -0
- package/lib/serviceStatus.mjs +143 -0
- package/lib/serviceStatus.test.mjs +136 -0
- package/lib/strayDividers.d.mts +6 -0
- package/lib/strayDividers.mjs +15 -0
- package/lib/strayDividers.test.mjs +25 -0
- package/package.json +5 -3
- package/release-notes/i18n/da-DK/pending/apps-turn-off.json +6 -0
- package/release-notes/i18n/da-DK/pending/apps-turn-off.webp +0 -0
- package/release-notes/i18n/da-DK/pending/developer-menu.json +5 -0
- package/release-notes/i18n/da-DK/pending/dynamic-lists-page.json +5 -0
- package/release-notes/i18n/da-DK/pending/editor-footer.json +6 -0
- package/release-notes/i18n/da-DK/pending/editor-footer.webp +0 -0
- package/release-notes/i18n/da-DK/pending/editor-toolbar-dock.json +5 -0
- package/release-notes/i18n/da-DK/pending/google-preview-closer.json +6 -0
- package/release-notes/i18n/da-DK/pending/google-preview-closer.webp +0 -0
- package/release-notes/i18n/da-DK/pending/morpheme-connection-page.json +5 -0
- package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.json +6 -0
- package/release-notes/i18n/da-DK/pending/quiet-lamps-wander.webp +0 -0
- package/release-notes/i18n/en-US/pending/apps-turn-off.json +6 -0
- package/release-notes/i18n/en-US/pending/apps-turn-off.webp +0 -0
- package/release-notes/i18n/en-US/pending/developer-menu.json +5 -0
- package/release-notes/i18n/en-US/pending/dynamic-lists-page.json +5 -0
- package/release-notes/i18n/en-US/pending/editor-footer.json +6 -0
- package/release-notes/i18n/en-US/pending/editor-footer.webp +0 -0
- package/release-notes/i18n/en-US/pending/editor-toolbar-dock.json +5 -0
- package/release-notes/i18n/en-US/pending/google-preview-closer.json +6 -0
- package/release-notes/i18n/en-US/pending/google-preview-closer.webp +0 -0
- package/release-notes/i18n/en-US/pending/morpheme-connection-page.json +5 -0
- package/release-notes/i18n/en-US/pending/quiet-lamps-wander.json +6 -0
- package/release-notes/i18n/en-US/pending/quiet-lamps-wander.webp +0 -0
- package/scripts/app-screenshots.mjs +4 -4
- package/scripts/build-settings-problems.mjs +1 -1
- package/scripts/built-site.mjs +1 -1
- package/scripts/check-content.mjs +1 -1
- package/scripts/check-release-notes.mjs +12 -0
- package/scripts/check-worker.mjs +1 -1
- package/scripts/editor-screens.mjs +32 -16
- package/scripts/release-notes-pending.mjs +46 -0
- package/scripts/release-notes.mjs +39 -0
- package/site/apps.ts +10 -4
- package/site/consent.ts +3 -3
- package/site/defineSite.mjs +1 -1
- package/site/icons.d.mts +4 -0
- package/site/icons.mjs +2 -0
- package/site/pageColors.ts +3 -3
- package/site/siteCsp.d.mts +1 -1
- package/site/siteCsp.mjs +3 -2
- package/src/ActionParts.tsx +1 -1
- package/src/Advanced.tsx +4 -4
- package/src/App.tsx +22 -17
- package/src/AppEnabledSwitch.tsx +21 -0
- package/src/AppList.tsx +47 -19
- package/src/AppSettingsTab.tsx +9 -5
- package/src/BlockParts.tsx +7 -7
- package/src/BlockSettings.tsx +3 -3
- package/src/CmsPage.tsx +11 -10
- package/src/Colors.tsx +2 -2
- package/src/CommunicationParts.tsx +18 -2
- package/src/ContainmentParts.tsx +1 -1
- package/src/DataForm.tsx +1 -0
- package/src/EditorFooter.tsx +19 -9
- package/src/EntryForm.tsx +1 -0
- package/src/FooterCopyright.tsx +7 -0
- package/src/FooterDivider.tsx +5 -0
- package/src/FooterLink.tsx +18 -0
- package/src/FooterLinks.tsx +34 -0
- package/src/Library.tsx +21 -10
- package/src/MainDrawerParts.tsx +1 -1
- package/src/MarkdownEditor.tsx +1 -1
- package/src/ModalSheetSample.tsx +1 -1
- package/src/MorphemeConnection.tsx +30 -0
- package/src/MorphemeLogo.tsx +9 -0
- package/src/NavigationParts.tsx +1 -1
- package/src/NavigationRestParts.tsx +1 -1
- package/src/Parts.tsx +9 -9
- package/src/PartsShown.tsx +1 -1
- package/src/PickerParts.tsx +1 -1
- package/src/ReleaseNoteCard.tsx +17 -0
- package/src/ReleaseNotes.tsx +52 -0
- package/src/Screen.tsx +21 -12
- package/src/SearchPreview.tsx +37 -14
- package/src/SelectionParts.tsx +1 -1
- package/src/ServiceStatus.tsx +59 -0
- package/src/SideColumn.tsx +2 -2
- package/src/StoreConnectNotice.tsx +1 -1
- package/src/StoreConnectedRow.tsx +20 -14
- package/src/StoreStatus.tsx +2 -10
- package/src/ThemeStoreScreen.tsx +1 -2
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +16 -14
- package/src/buildSettings.ts +1 -1
- package/src/cmsView.d.mts +1 -0
- package/src/cmsView.mjs +4 -2
- package/src/cmsView.test.mjs +6 -0
- package/src/editor.css +13 -2
- package/src/examples.ts +2 -2
- package/src/fabPresence.tsx +24 -0
- package/src/googleResult.d.mts +12 -1
- package/src/googleResult.mjs +19 -2
- package/src/i18n/da-DK/apps.ts +5 -1
- package/src/i18n/da-DK/cms.ts +3 -0
- package/src/i18n/da-DK/common.ts +3 -2
- package/src/i18n/da-DK/footer.ts +26 -0
- package/src/i18n/da-DK/index.ts +2 -0
- package/src/i18n/da-DK/parts.ts +5 -0
- package/src/i18n/en-US/advanced.ts +1 -1
- package/src/i18n/en-US/apps.ts +6 -1
- package/src/i18n/en-US/cms.ts +5 -2
- package/src/i18n/en-US/common.ts +6 -4
- package/src/i18n/en-US/footer.ts +24 -0
- package/src/i18n/en-US/index.ts +2 -0
- package/src/i18n/en-US/parts.ts +8 -3
- package/src/menuChildren.ts +6 -14
- package/src/movedRoutes.mjs +12 -8
- package/src/movedRoutes.test.mjs +13 -10
- package/src/r2Caching.ts +1 -1
- package/src/registry.tsx +2 -2
- package/src/schema.ts +1 -1
- package/src/site.ts +2 -2
- package/src/statusReport.ts +24 -0
- package/src/store.ts +4 -8
- package/src/themeUi.ts +7 -7
- package/src/ui.tsx +4 -2
- package/src/useFooterClearance.ts +30 -0
- package/src/useStrayDividers.ts +24 -0
- package/theme/components/AppSlot.astro +4 -4
- package/theme/styles/global.css +10 -0
- package/theme/styles/ui.css +13 -0
- package/theme/ui/Badge.tsx +9 -3
- package/worker/cms-update.js +49 -12
- package/worker/index.js +10 -4
- package/worker/plan-status.js +1 -1
- package/worker/r2-status.js +1 -1
- package/worker/service-status.js +18 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// lib/serviceStatus.mjs tests; run with: node --test lib/serviceStatus.test.mjs
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { PROVIDERS, providersFor, stateOf, worst, providerFrom, reportFrom } from './serviceStatus.mjs';
|
|
5
|
+
|
|
6
|
+
test('stateOf maps statuses', () => {
|
|
7
|
+
assert.equal(stateOf('operational'), 'ok');
|
|
8
|
+
assert.equal(stateOf('degraded_performance'), 'minor');
|
|
9
|
+
assert.equal(stateOf('under_maintenance'), 'minor');
|
|
10
|
+
assert.equal(stateOf('partial_outage'), 'major');
|
|
11
|
+
assert.equal(stateOf('major_outage'), 'major');
|
|
12
|
+
assert.equal(stateOf('foo'), 'unknown');
|
|
13
|
+
assert.equal(stateOf(undefined), 'unknown');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('worst picks most severe', () => {
|
|
17
|
+
assert.equal(worst([]), 'ok');
|
|
18
|
+
assert.equal(worst(['ok', 'unknown']), 'unknown');
|
|
19
|
+
assert.equal(worst(['unknown', 'minor']), 'minor');
|
|
20
|
+
assert.equal(worst(['minor', 'major', 'ok']), 'major');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('providerFrom with null summary', () => {
|
|
24
|
+
const p = providerFrom(PROVIDERS[0], null);
|
|
25
|
+
assert.equal(p.state, 'unknown');
|
|
26
|
+
assert.equal(p.components.length, 3);
|
|
27
|
+
for (const c of p.components) {
|
|
28
|
+
assert.equal(c.status, 'unknown');
|
|
29
|
+
assert.equal(c.state, 'unknown');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('providerFrom with partial feed', () => {
|
|
34
|
+
const feed = { components: [{ id: '8l4ygp009s5s', name: 'x', status: 'major_outage' }] };
|
|
35
|
+
const p = providerFrom(PROVIDERS[0], feed);
|
|
36
|
+
assert.equal(p.state, 'major');
|
|
37
|
+
assert.equal(p.components[0].name, 'Git Operations');
|
|
38
|
+
assert.equal(p.components[0].status, 'major_outage');
|
|
39
|
+
assert.equal(p.components[0].state, 'major');
|
|
40
|
+
assert.equal(p.components[1].status, 'unknown');
|
|
41
|
+
assert.equal(p.components[2].status, 'unknown');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('providerFrom ignores extra components', () => {
|
|
45
|
+
const s = {
|
|
46
|
+
components: [
|
|
47
|
+
{ id: '57srcl8zcn7c', name: 'Workers', status: 'operational' },
|
|
48
|
+
{ id: 'pg21w9xb991r', name: 'Workers Assets', status: 'operational' },
|
|
49
|
+
{ id: 'vcpsk0w6g86k', name: 'Workers Builds', status: 'operational' },
|
|
50
|
+
{ id: 'tmh50tx2nprs', name: 'Workers KV', status: 'operational' },
|
|
51
|
+
{ id: 'z0yd3sxqjqdm', name: 'D1', status: 'operational' },
|
|
52
|
+
{ id: 'hb7g5sq2zz0h', name: 'R2', status: 'operational' },
|
|
53
|
+
{ id: 'other', name: 'Other', status: 'major_outage' },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
const p = providerFrom(PROVIDERS[1], s);
|
|
57
|
+
assert.equal(p.state, 'ok');
|
|
58
|
+
assert.equal(p.components.length, 6);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('providerFrom maps unknown status to unknown', () => {
|
|
62
|
+
const feed = { components: [{ id: '8l4ygp009s5s', status: 'weird' }] };
|
|
63
|
+
const p = providerFrom(PROVIDERS[0], feed);
|
|
64
|
+
assert.equal(p.components[0].status, 'unknown');
|
|
65
|
+
assert.equal(p.components[0].state, 'unknown');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('reportFrom with all nulls', () => {
|
|
69
|
+
const r = reportFrom({ github: null, cloudflare: null });
|
|
70
|
+
assert.equal(r.state, 'unknown');
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
r.providers.map((p) => p.id),
|
|
73
|
+
['github', 'cloudflare'],
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('reportFrom with minor degradation', () => {
|
|
78
|
+
const github = {
|
|
79
|
+
components: [
|
|
80
|
+
{ id: '8l4ygp009s5s', name: 'Git Operations', status: 'operational' },
|
|
81
|
+
{ id: 'brv1bkgrwx7q', name: 'API Requests', status: 'operational' },
|
|
82
|
+
{ id: '4230lsnqdsld', name: 'Webhooks', status: 'operational' },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
const cloudflare = {
|
|
86
|
+
components: [
|
|
87
|
+
{ id: '57srcl8zcn7c', name: 'Workers', status: 'operational' },
|
|
88
|
+
{ id: 'pg21w9xb991r', name: 'Workers Assets', status: 'operational' },
|
|
89
|
+
{ id: 'vcpsk0w6g86k', name: 'Workers Builds', status: 'operational' },
|
|
90
|
+
{ id: 'tmh50tx2nprs', name: 'Workers KV', status: 'operational' },
|
|
91
|
+
{ id: 'z0yd3sxqjqdm', name: 'D1', status: 'operational' },
|
|
92
|
+
{ id: 'hb7g5sq2zz0h', name: 'R2', status: 'degraded_performance' },
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
const r = reportFrom({ github, cloudflare });
|
|
96
|
+
assert.equal(r.state, 'minor');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const cloudflareIds = (report) => report.providers.find((p) => p.id === 'cloudflare').components.map((c) => c.name);
|
|
100
|
+
const feed = (id, status) => ({ components: [{ id, status }] });
|
|
101
|
+
const ALL = ['NOT_FOUND', 'PUSH', 'FILES', 'CONSENT'];
|
|
102
|
+
|
|
103
|
+
test('without FILES there is no R2, and an R2 outage in the feed does not count', () => {
|
|
104
|
+
const report = reportFrom({ github: null, cloudflare: feed('hb7g5sq2zz0h', 'major_outage') }, providersFor(['NOT_FOUND', 'CONSENT']));
|
|
105
|
+
assert.equal(cloudflareIds(report).includes('R2'), false);
|
|
106
|
+
assert.notEqual(report.providers.find((p) => p.id === 'cloudflare').state, 'major');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('with FILES R2 is there, and its outage counts', () => {
|
|
110
|
+
const report = reportFrom({ github: null, cloudflare: feed('hb7g5sq2zz0h', 'major_outage') }, providersFor(ALL));
|
|
111
|
+
assert.equal(cloudflareIds(report).includes('R2'), true);
|
|
112
|
+
assert.equal(report.providers.find((p) => p.id === 'cloudflare').state, 'major');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('D1 counts only with CONSENT', () => {
|
|
116
|
+
assert.equal(cloudflareIds(reportFrom({}, providersFor(['FILES', 'PUSH']))).includes('D1'), false);
|
|
117
|
+
assert.equal(cloudflareIds(reportFrom({}, providersFor(['CONSENT']))).includes('D1'), true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('Workers KV counts with NOT_FOUND or PUSH, and with neither it does not', () => {
|
|
121
|
+
assert.equal(cloudflareIds(reportFrom({}, providersFor(['NOT_FOUND']))).includes('Workers KV'), true);
|
|
122
|
+
assert.equal(cloudflareIds(reportFrom({}, providersFor(['PUSH']))).includes('Workers KV'), true);
|
|
123
|
+
assert.equal(cloudflareIds(reportFrom({}, providersFor(['FILES', 'CONSENT']))).includes('Workers KV'), false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('with no binding the three Workers components stay, and GitHub does not change', () => {
|
|
127
|
+
const report = reportFrom({}, providersFor([]));
|
|
128
|
+
assert.deepEqual(cloudflareIds(report), ['Workers', 'Workers Assets', 'Workers Builds']);
|
|
129
|
+
assert.equal(report.providers.find((p) => p.id === 'github').components.length, PROVIDERS[0].components.length);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('reportFrom without a list reports every component, and a report has no binding names', () => {
|
|
133
|
+
const report = reportFrom({});
|
|
134
|
+
assert.equal(cloudflareIds(report).length, 6);
|
|
135
|
+
assert.equal(JSON.stringify(report).includes('needs'), false);
|
|
136
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {number[]} rows The vertical position of each item in the row, in order, items and dividers alternating, starting
|
|
3
|
+
* and ending with an item (the same value for items on the same line).
|
|
4
|
+
* @returns {number[]} The indexes of the dividers (the odd ones) that are first or last on their line.
|
|
5
|
+
*/
|
|
6
|
+
export function strayDividers(rows: number[]): number[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Which dividers in a wrapping row of items stand at the start or the end of a line (and so separate nothing).
|
|
2
|
+
// Exposes strayDividers. No imports.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {number[]} rows The vertical position of each item in the row, in order, items and dividers alternating, starting
|
|
6
|
+
* and ending with an item (the same value for items on the same line).
|
|
7
|
+
* @returns {number[]} The indexes of the dividers (the odd ones) that are first or last on their line.
|
|
8
|
+
*/
|
|
9
|
+
export function strayDividers(rows) {
|
|
10
|
+
const stray = [];
|
|
11
|
+
for (let i = 1; i < rows.length - 1; i += 2) {
|
|
12
|
+
if (rows[i] !== rows[i - 1] || rows[i] !== rows[i + 1]) stray.push(i);
|
|
13
|
+
}
|
|
14
|
+
return stray;
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Tests for lib/strayDividers.mjs: a divider is hidden when it is first or last on its line.
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { strayDividers } from './strayDividers.mjs';
|
|
5
|
+
|
|
6
|
+
test('on one line no divider is stray', () => {
|
|
7
|
+
assert.deepEqual(strayDividers([0, 0, 0, 0, 0]), []);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('a divider that ends a line is stray', () => {
|
|
11
|
+
assert.deepEqual(strayDividers([0, 0, 0, 0, 24]), [3]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('a divider that starts a line is stray', () => {
|
|
15
|
+
assert.deepEqual(strayDividers([0, 0, 0, 24, 24]), [3]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('a divider that ends one line and one that starts the next are both stray', () => {
|
|
19
|
+
assert.deepEqual(strayDividers([0, 0, 0, 0, 24, 24, 24]), [3]);
|
|
20
|
+
assert.deepEqual(strayDividers([0, 0, 0, 24, 24, 24, 48]), [3, 5]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('a row of one item has none', () => {
|
|
24
|
+
assert.deepEqual(strayDividers([0]), []);
|
|
25
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphemecms/cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0-next-20261107093528",
|
|
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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"icons",
|
|
25
25
|
"lib",
|
|
26
26
|
"pages",
|
|
27
|
+
"release-notes",
|
|
27
28
|
"scripts",
|
|
28
29
|
"!scripts/starter.mjs",
|
|
29
30
|
"!scripts/check-starter.mjs",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "node scripts/demo-build.mjs",
|
|
43
|
-
"check": "npm run format:check && npm run check:language && npm test && npm run check:types && npm run build && npm run check:site-types && npm run check:editor",
|
|
44
|
+
"check": "npm run format:check && npm run check:language && npm run check:release-notes && npm test && npm run check:types && npm run build && npm run check:site-types && npm run check:editor",
|
|
44
45
|
"check:site-types": "node scripts/check-site-types.mjs",
|
|
45
46
|
"check:types": "tsc -p tsconfig.types.json --noEmit",
|
|
46
47
|
"check:editor": "cd .morpheme/demo/site && node ../../../bin/morpheme.mjs check:editor",
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
"format": "prettier --write .",
|
|
53
54
|
"format:check": "prettier --check .",
|
|
54
55
|
"check:language": "node scripts/check-language.mjs",
|
|
55
|
-
"release
|
|
56
|
+
"check:release-notes": "node scripts/check-release-notes.mjs",
|
|
57
|
+
"release:version": "changeset version && node scripts/release-notes.mjs && node scripts/starter.mjs",
|
|
56
58
|
"prepack": "node scripts/types.mjs write",
|
|
57
59
|
"postpack": "node scripts/types.mjs clean"
|
|
58
60
|
},
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "apps-turn-off",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "Slå en app fra uden at miste den",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "editor-footer",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "Se om GitHub og Cloudflare virker",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-preview-closer",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "En Google-forhåndsvisning, der ligner Google",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "quiet-lamps-wander",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "Se, hvad der er nyt, med billeder",
|
|
5
|
+
"text": "Opdateringer skal føles som en venlig hilsen, ikke som en udviklerlog. Åbn siden Morpheme, så ser du hver nyhed med et billede og et par almindelige ord. Alt, der er ændret i din version, står lige der."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "apps-turn-off",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "Turn an app off without losing it",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "editor-footer",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "See if GitHub and Cloudflare are working",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-preview-closer",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "A Google preview that looks like Google",
|
|
5
|
+
"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."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "quiet-lamps-wander",
|
|
3
|
+
"kind": "card",
|
|
4
|
+
"title": "See what's new, with pictures",
|
|
5
|
+
"text": "Updates should feel like a friendly hello, not a developer log. Open the Morpheme page and you see each new thing with a picture and a few plain words. Everything that changed in your version is right there."
|
|
6
|
+
}
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Takes the pictures each app is shown with on its page and in the app store (AppAbout in
|
|
2
2
|
// packages/editor/src/AppList.tsx): the few its index.ts picks to give an idea of it (`screenshots`, AppShot in
|
|
3
3
|
// packages/editor/src/store.ts), as a theme's are, on a phone, in the standard theme with blue colours and with made-up
|
|
4
|
-
// content only, never the site's: its blocks and kinds as "
|
|
4
|
+
// content only, never the site's: its blocks and kinds as "Developer" › "Parts" › "Apps" draws them with the examples,
|
|
5
5
|
// its lists, its settings (with the made-up `values` of the picture) and its own page in the editor, into
|
|
6
6
|
// src/apps/<app>/screenshots/<n>-<what>.jpg (shotFile). The site is built in a folder of its own with the theme and the
|
|
7
7
|
// colours (src/data/theme.json and colors.json as they were after), and the editor has every app installed, with GitHub
|
|
@@ -30,8 +30,8 @@ const BLUE = { source: '#1a73e8', variant: 'tonalSpot', contrast: 'standard' };
|
|
|
30
30
|
|
|
31
31
|
const ids = process.argv.length > 2 ? process.argv.slice(2) : readdirSync(APPS).filter((id) => existsSync(join(APPS, id, 'index.ts')));
|
|
32
32
|
|
|
33
|
-
// The pictures each app picks, read as store-release.mjs does, in their order. `part`: its drawing under "
|
|
34
|
-
// "Blokke" or the list's screen (`parts`: #
|
|
33
|
+
// The pictures each app picks, read as store-release.mjs does, in their order. `part`: its drawing under "Parts" ›
|
|
34
|
+
// "Blokke" or the list's screen (`parts`: #developer/parts/blocks, #developer/parts/list/<list>) by the part's name; `editor`: a screen of the editor (`at`: from the top of what is
|
|
35
35
|
// there). And each app's settings as the pictures show them: those it starts with, and the made-up ones of its picture.
|
|
36
36
|
const vite = await createServer({
|
|
37
37
|
configFile: false,
|
|
@@ -136,7 +136,7 @@ try {
|
|
|
136
136
|
for (const [i, shot] of shots[id].entries()) {
|
|
137
137
|
const options = { path: join(folder, shot.name), type: 'jpeg', quality: 80 };
|
|
138
138
|
if (shot.part) {
|
|
139
|
-
await go(`
|
|
139
|
+
await go(`developer/parts/${shot.parts}`);
|
|
140
140
|
// Tall enough for the whole drawing, under the screen's top bar.
|
|
141
141
|
await editor.setViewportSize({ width: phone.width, height: 1600 });
|
|
142
142
|
const drawing = editor.locator(`section[data-part="${shot.part}"] > :last-child`).first();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The check of the build settings file (src/data/build.json,
|
|
1
|
+
// The check of the build settings file (src/data/build.json, Settings › Developer › Caching and builds): an object with only the
|
|
2
2
|
// boolean keys contentLane and paidBuilds. Exposes buildSettingsProblems; used by check-content.mjs. No dependencies.
|
|
3
3
|
|
|
4
4
|
const KEYS = ['contentLane', 'paidBuilds'];
|
package/scripts/built-site.mjs
CHANGED
|
@@ -138,7 +138,7 @@ export const STORE_STATES = {
|
|
|
138
138
|
* store (a key of STORE_STATES). */
|
|
139
139
|
export async function openEditor(page, site, texts = {}, store = 'button') {
|
|
140
140
|
let state = STORE_STATES[store];
|
|
141
|
-
// R2 caching under "
|
|
141
|
+
// R2 caching under "Caching and builds": Cloudflare has the bucket (packages/editor/worker/r2-status.js).
|
|
142
142
|
await page.route(`${site}/api/r2?**`, (r) =>
|
|
143
143
|
r.fulfill({ contentType: 'application/json', body: JSON.stringify({ bound: false, bucket: 'ready' }) }),
|
|
144
144
|
);
|
|
@@ -185,7 +185,7 @@ try {
|
|
|
185
185
|
tracked = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
186
186
|
} catch {}
|
|
187
187
|
result.problems.push(...casingProblems(tracked));
|
|
188
|
-
// The build settings the editor writes under "
|
|
188
|
+
// The build settings the editor writes under "Caching and builds": only boolean contentLane and paidBuilds; a missing file passes.
|
|
189
189
|
result.problems.push(
|
|
190
190
|
...buildSettingsProblems(BUILD_SETTINGS_PATH, existsSync(BUILD_SETTINGS_PATH) ? readFileSync(BUILD_SETTINGS_PATH, 'utf8') : undefined),
|
|
191
191
|
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Checks the pending release notes: each note has its twin in every locale with the same kind, an id that matches its file name,
|
|
2
|
+
// and the fields and screenshot its kind needs (a card: title, text and a screenshot of at most 200 KB; a line: text only).
|
|
3
|
+
// Run by npm run check:release-notes; usage: node scripts/check-release-notes.mjs [root]. Exits 1 and prints each problem.
|
|
4
|
+
import { problemsOf } from '../lib/releaseNotes.mjs';
|
|
5
|
+
import { readPending, repoRoot } from './release-notes-pending.mjs';
|
|
6
|
+
|
|
7
|
+
const problems = problemsOf(readPending(repoRoot(process.argv[2])));
|
|
8
|
+
if (problems.length > 0) {
|
|
9
|
+
for (const problem of problems) console.error(problem);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
console.log('Release notes: no problems.');
|
package/scripts/check-worker.mjs
CHANGED
|
@@ -472,7 +472,7 @@ try {
|
|
|
472
472
|
await run('r2-fejler', failing);
|
|
473
473
|
await settle();
|
|
474
474
|
|
|
475
|
-
// R2 caching under
|
|
475
|
+
// R2 caching under Settings › Developer › Caching and builds (r2Caching.ts): the Worker's settings have the line and its bucket, and
|
|
476
476
|
// with it on or off they are settings wrangler reads, with the bucket bound as FILES or no bucket; on and off again,
|
|
477
477
|
// they are as they were. And /api/r2 (packages/editor/worker/r2-status.js) says what Cloudflare says of the bucket, only to someone who
|
|
478
478
|
// can save, so the switch is turned on only when Cloudflare has it.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// packages/editor/src/icon.tsx), there or on one of the built site's pages. An icon drawn from Material Symbols instead
|
|
5
5
|
// of the icon pack (data-icon-fallback) is only listed, with where it is: it does not fail.
|
|
6
6
|
// The screens are found as the editor's own links: from the start screen (the drawer's links are in the page also while it is
|
|
7
|
-
// closed) and "
|
|
7
|
+
// closed) and "Parts", #developer/parts, "Temaer", #appearance/themes, "Søgemaskineoptimering", #seo, "Appstore", #settings/appstore, and "Apps", #apps, which the menu no longer links to), and
|
|
8
8
|
// from each screen it opens, two steps down. GitHub is answered from the files in this repository, so it needs no login
|
|
9
9
|
// and changes nothing. It runs on the built site: npm run build && npm run check:screens (or on another
|
|
10
10
|
// folder with a built site, its path after the command: packages/editor/scripts/check-themes.mjs).
|
|
@@ -68,10 +68,10 @@ for (const [width, height] of [
|
|
|
68
68
|
});
|
|
69
69
|
await openEditor(page, site);
|
|
70
70
|
|
|
71
|
-
// From the start screen (the drawer's links), and from "
|
|
71
|
+
// From the start screen (the drawer's links), and from "Parts", "Temaer", "Søgemaskineoptimering" and "Appstore"
|
|
72
72
|
// (the apps' pages and their developers'), whose screens are a step further down than the others. "Apps" (the list of
|
|
73
73
|
// the apps installed) has no row in the menu, so it is a start of its own.
|
|
74
|
-
const starts = ['', '
|
|
74
|
+
const starts = ['', 'developer/parts', 'appearance/themes', 'seo', 'settings/appstore', 'apps'];
|
|
75
75
|
const seen = new Set(starts);
|
|
76
76
|
let next = starts;
|
|
77
77
|
for (let depth = 0; depth <= 2 && next.length; depth++) {
|
|
@@ -104,27 +104,43 @@ for (const [width, height] of [
|
|
|
104
104
|
await page.close();
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// "Appstore" in the three states of the site's key to the store (STORE_STATES): what each shows, and a picture of it in
|
|
107
|
+
// "Appstore" and "Morpheme" in the three states of the site's key to the store (STORE_STATES): what each shows, and a picture of it in
|
|
108
108
|
// SCREENS_DIR (the temporary folder's morpheme-screens by default).
|
|
109
109
|
const shots = process.env.SCREENS_DIR || join(tmpdir(), 'morpheme-screens');
|
|
110
110
|
mkdirSync(shots, { recursive: true });
|
|
111
111
|
const keyStates = {
|
|
112
|
-
open: {
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
open: {
|
|
113
|
+
appstore: {
|
|
114
|
+
shows: ['Hjemmesiden er ikke forbundet til Morpheme', 'Forbind til Morpheme'],
|
|
115
|
+
hides: ['Afbryd forbindelsen til Morpheme'],
|
|
116
|
+
},
|
|
117
|
+
morpheme: { shows: ['Forbind til Morpheme'], hides: ['Afbryd forbindelsen til Morpheme'] },
|
|
118
|
+
},
|
|
119
|
+
readOnly: {
|
|
120
|
+
appstore: { shows: ['Hjemmesiden har ingen Appstore'], hides: ['Forbind til Morpheme', 'Afbryd forbindelsen til Morpheme'] },
|
|
121
|
+
morpheme: { shows: ['Hjemmesiden har ingen Appstore'], hides: ['Forbind til Morpheme', 'Afbryd forbindelsen til Morpheme'] },
|
|
122
|
+
},
|
|
123
|
+
button: {
|
|
124
|
+
appstore: { shows: [], hides: ['Forbundet til Morpheme', 'Afbryd forbindelsen til Morpheme'] },
|
|
125
|
+
morpheme: { shows: ['Forbundet til Morpheme', 'site_sample', 'Afbryd forbindelsen til Morpheme'], hides: ['Forbind til Morpheme'] },
|
|
126
|
+
},
|
|
115
127
|
};
|
|
116
|
-
|
|
128
|
+
// The screens that show the key: where each is (its address after the "#", CMS_ROUTE in CmsPage.tsx for "Morpheme").
|
|
129
|
+
const keyScreens = { appstore: 'settings/appstore', morpheme: 'morpheme' };
|
|
130
|
+
for (const [state, checks] of Object.entries(keyStates)) {
|
|
117
131
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, locale: 'da-DK' });
|
|
118
132
|
page.on('pageerror', (e) => problems.push(`Appstore (${state}): ${e.message}`));
|
|
119
133
|
await openEditor(page, site, {}, state);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
for (const [screen, { shows, hides }] of Object.entries(checks)) {
|
|
135
|
+
await page.evaluate((hash) => (location.hash = hash), keyScreens[screen]);
|
|
136
|
+
await page.waitForTimeout(500);
|
|
137
|
+
const text = await page.evaluate(() => document.body.innerText);
|
|
138
|
+
for (const x of shows) if (!text.includes(x)) problems.push(`${screen} (${state}): "${x}" mangler`);
|
|
139
|
+
for (const x of hides) if (text.includes(x)) problems.push(`${screen} (${state}): "${x}" skal ikke vises`);
|
|
140
|
+
const file = join(shots, `${screen}-${state}.png`);
|
|
141
|
+
await page.screenshot({ path: file });
|
|
142
|
+
console.log(`${screen} (${state}): ${file}`);
|
|
143
|
+
}
|
|
128
144
|
await page.close();
|
|
129
145
|
}
|
|
130
146
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Reads the pending release notes of a repository (release-notes/i18n/<locale>/pending/<id>.json, and <id>.webp for a card)
|
|
2
|
+
// into the Pending object of lib/releaseNotes.mjs, and resolves the repository root. Exposes readPending(root) and repoRoot(arg).
|
|
3
|
+
// Depends on node:fs, node:path, node:url and webpSize of lib/releaseNotes.mjs; shared by release-notes.mjs and check-release-notes.mjs.
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { webpSize } from '../lib/releaseNotes.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads every pending note of every locale, with the size of its screenshot in bytes and in pixels.
|
|
11
|
+
* @param {string} root the repository root
|
|
12
|
+
* @returns {Record<string, Record<string, { note: object, imageBytes: number | null, imageSize: { width: number, height: number } | null }>>} locale -> id -> entry; {} when there are no folders
|
|
13
|
+
*/
|
|
14
|
+
export function readPending(root) {
|
|
15
|
+
const i18n = join(root, 'release-notes', 'i18n');
|
|
16
|
+
const pending = {};
|
|
17
|
+
if (!existsSync(i18n)) return pending;
|
|
18
|
+
for (const locale of readdirSync(i18n, { withFileTypes: true })) {
|
|
19
|
+
const dir = join(i18n, locale.name, 'pending');
|
|
20
|
+
if (!locale.isDirectory() || !existsSync(dir)) continue;
|
|
21
|
+
pending[locale.name] = {};
|
|
22
|
+
const files = readdirSync(dir)
|
|
23
|
+
.filter((name) => name.endsWith('.json'))
|
|
24
|
+
.sort();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const id = file.slice(0, -'.json'.length);
|
|
27
|
+
const image = join(dir, `${id}.webp`);
|
|
28
|
+
const bytes = existsSync(image) ? readFileSync(image) : null;
|
|
29
|
+
pending[locale.name][id] = {
|
|
30
|
+
note: JSON.parse(readFileSync(join(dir, file), 'utf8')),
|
|
31
|
+
imageBytes: bytes ? bytes.length : null,
|
|
32
|
+
imageSize: bytes ? webpSize(bytes) : null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return pending;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The repository root: the argument when given, else the folder above this script.
|
|
41
|
+
* @param {string | undefined} arg
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function repoRoot(arg) {
|
|
45
|
+
return resolve(arg ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Release step: moves the pending release notes into release-notes/i18n/<locale>/<version>.json, their screenshots into
|
|
2
|
+
// <version>/<id>.webp, the version from package.json. Run by release:version after changeset version.
|
|
3
|
+
// Usage: node scripts/release-notes.mjs [root]; exits 1 on a problem or an existing file.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { imagePath, problemsOf, released } from '../lib/releaseNotes.mjs';
|
|
7
|
+
import { readPending, repoRoot } from './release-notes-pending.mjs';
|
|
8
|
+
|
|
9
|
+
const root = repoRoot(process.argv[2]);
|
|
10
|
+
const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
11
|
+
const pending = readPending(root);
|
|
12
|
+
|
|
13
|
+
const problems = problemsOf(pending);
|
|
14
|
+
if (problems.length > 0) {
|
|
15
|
+
for (const problem of problems) console.error(problem);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = released(pending, version);
|
|
20
|
+
const targets = Object.keys(result).map((locale) => join(root, 'release-notes', 'i18n', locale, `${version}.json`));
|
|
21
|
+
const existing = targets.filter((target) => existsSync(target));
|
|
22
|
+
if (existing.length > 0) {
|
|
23
|
+
for (const target of existing) console.error(`${target} exists; a released version is never changed`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [locale, { file, images }] of Object.entries(result)) {
|
|
28
|
+
const localeDir = join(root, 'release-notes', 'i18n', locale);
|
|
29
|
+
writeFileSync(join(localeDir, `${version}.json`), `${JSON.stringify(file, null, 2)}\n`);
|
|
30
|
+
for (const id of images) {
|
|
31
|
+
const target = join(root, imagePath(version, locale, id));
|
|
32
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
33
|
+
renameSync(join(localeDir, 'pending', `${id}.webp`), target);
|
|
34
|
+
}
|
|
35
|
+
for (const id of Object.keys(pending[locale])) {
|
|
36
|
+
rmSync(join(localeDir, 'pending', `${id}.json`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log(`Release notes for ${version}: ${Object.keys(result).join(', ') || 'none'}.`);
|