@morphemecms/cms 0.14.2-next-20261107121812 → 0.14.2
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 +3 -1
- package/lib/releaseNotes.d.mts +2 -0
- package/lib/releaseNotes.mjs +5 -1
- package/lib/releaseNotes.test.mjs +11 -0
- package/package.json +1 -1
- package/release-notes/i18n/da-DK/0.14.2.json +21 -0
- package/release-notes/i18n/en-US/0.14.2.json +21 -0
- package/src/Screen.tsx +7 -8
- package/src/editor.css +9 -0
- package/src/fabPresence.tsx +1 -1
- package/src/topBarSlot.tsx +3 -2
- package/starter/package.json +1 -1
- package/lib/footerClearance.d.mts +0 -1
- package/lib/footerClearance.mjs +0 -9
- package/lib/footerClearance.test.mjs +0 -24
- package/release-notes/i18n/da-DK/pending/lists-follow-md3.json +0 -5
- package/release-notes/i18n/da-DK/pending/update-card-whats-new.json +0 -6
- package/release-notes/i18n/en-US/pending/lists-follow-md3.json +0 -5
- package/release-notes/i18n/en-US/pending/update-card-whats-new.json +0 -6
- package/src/useFooterClearance.ts +0 -30
- /package/release-notes/i18n/da-DK/{pending → 0.14.2}/update-card-whats-new.webp +0 -0
- /package/release-notes/i18n/en-US/{pending → 0.14.2}/update-card-whats-new.webp +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
-
## 0.14.2
|
|
3
|
+
## 0.14.2
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
+
- fc6864a: On a phone, the editor's footer starts at the bottom of a short screen, and you can scroll it up above the add button.
|
|
7
8
|
- 13dbbca: Lists follow Material 3's list spec: the label text in body large (16/24, weight 400), also beside a switch, and the editor's rows on Surface over a page in Surface Container.
|
|
9
|
+
- 5c21889: The editor of a test version (npm tag `next`, e.g. 0.14.2-next.0) loads again: the release notes it carries no longer compare a test version, which threw and left the page blank.
|
|
8
10
|
- 3fbb65f: The start screen's update card shows the new version and its first news, and the Morpheme page shows the release notes as an article.
|
|
9
11
|
|
|
10
12
|
## 0.14.1
|
package/lib/releaseNotes.d.mts
CHANGED
|
@@ -54,6 +54,8 @@ export function notesSince(entries: {
|
|
|
54
54
|
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
55
55
|
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
56
56
|
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
57
|
+
* A test version (0.14.2-next.0, the npm tag next) has not had the notes of its own version yet: the line up to the one before.
|
|
58
|
+
* It runs as the editor loads (bundledNotes.ts), so it never throws: nothing when `version` is not a version.
|
|
57
59
|
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
58
60
|
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
59
61
|
* @param {string} version the running version
|
package/lib/releaseNotes.mjs
CHANGED
|
@@ -180,18 +180,22 @@ const sameMinor = (a, b) => a.split('.').slice(0, 2).join('.') === b.split('.').
|
|
|
180
180
|
* The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
|
|
181
181
|
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
|
|
182
182
|
* (<locale>/<version>/<id>.webp) when that file exists. Versions of another minor, newer versions and versions without notes are left out.
|
|
183
|
+
* A test version (0.14.2-next.0, the npm tag next) has not had the notes of its own version yet: the line up to the one before.
|
|
184
|
+
* It runs as the editor loads (bundledNotes.ts), so it never throws: nothing when `version` is not a version.
|
|
183
185
|
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
184
186
|
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
185
187
|
* @param {string} version the running version
|
|
186
188
|
* @returns {{ version: string, text: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
187
189
|
*/
|
|
188
190
|
export function bundledNotes(files, images, version) {
|
|
191
|
+
const [base, pre] = String(version ?? '').split('-');
|
|
192
|
+
if (!PLAIN.test(base)) return [];
|
|
189
193
|
const byVersion = {};
|
|
190
194
|
for (const [path, file] of Object.entries(files)) {
|
|
191
195
|
const match = /\/i18n\/([^/]+)\/([^/]+)\.json$/.exec(path);
|
|
192
196
|
if (!match || !Array.isArray(file?.notes)) continue;
|
|
193
197
|
const [, locale, v] = match;
|
|
194
|
-
if (!PLAIN.test(v) || !sameMinor(v,
|
|
198
|
+
if (!PLAIN.test(v) || !sameMinor(v, base) || compareVersions(v, base) >= (pre === undefined ? 1 : 0)) continue;
|
|
195
199
|
byVersion[v] ??= {};
|
|
196
200
|
byVersion[v][locale] = file.notes.map((note) => {
|
|
197
201
|
if (note?.kind !== 'card') return note;
|
|
@@ -224,6 +224,17 @@ test('bundledNotes ignores a newer version', () => {
|
|
|
224
224
|
['0.14.0'],
|
|
225
225
|
);
|
|
226
226
|
});
|
|
227
|
+
test('bundledNotes takes a test version (npm tag next) without throwing: the line before its own version', () => {
|
|
228
|
+
const files = { ...en('0.14.0', [line('a')]), ...en('0.14.1', [line('b')]), ...en('0.14.2', [line('c')]) };
|
|
229
|
+
assert.deepEqual(
|
|
230
|
+
bundledNotes(files, {}, '0.14.2-next.0').map((e) => e.version),
|
|
231
|
+
['0.14.1', '0.14.0'],
|
|
232
|
+
);
|
|
233
|
+
assert.deepEqual(bundledNotes(files, {}, '0.15.0-next.3'), []);
|
|
234
|
+
});
|
|
235
|
+
test('bundledNotes gives nothing for something that is not a version', () => {
|
|
236
|
+
assert.deepEqual(bundledNotes(en('0.14.0', [line('a')]), {}, 'latest'), []);
|
|
237
|
+
});
|
|
227
238
|
test('bundledNotes gives an empty list without a file for the line', () => {
|
|
228
239
|
assert.deepEqual(bundledNotes(en('0.13.0', [line('a')]), {}, '0.14.0'), []);
|
|
229
240
|
assert.deepEqual(bundledNotes({}, {}, '0.14.0'), []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphemecms/cms",
|
|
3
|
-
"version": "0.14.2
|
|
3
|
+
"version": "0.14.2",
|
|
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,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.2",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "footer-scrolls-above-fab",
|
|
6
|
+
"kind": "line",
|
|
7
|
+
"text": "På en telefon kan du nu rulle editorens bundlinje fri af plusknappen, også på korte skærme."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "lists-follow-md3",
|
|
11
|
+
"kind": "line",
|
|
12
|
+
"text": "Lister i editoren viser nu lyse rækker på en tonet side, som i Material 3."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "update-card-whats-new",
|
|
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."
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.14.2",
|
|
3
|
+
"notes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "footer-scrolls-above-fab",
|
|
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."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "lists-follow-md3",
|
|
11
|
+
"kind": "line",
|
|
12
|
+
"text": "Lists in the editor now show light rows on a tinted page, as in Material 3."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "update-card-whats-new",
|
|
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."
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/src/Screen.tsx
CHANGED
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
// a form with its fields beside the text (`panel`). The column (editor.css .editor-column) stands centred on the window,
|
|
13
13
|
// off-centre only as far as the panels beside it require.
|
|
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
|
-
// is short (the screen fills the column under the bar, TopBarLayout)
|
|
16
|
-
//
|
|
17
|
-
|
|
15
|
+
// is short (the screen fills the column under the bar, TopBarLayout). Below medium, while the screen has a floating action
|
|
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
|
|
18
|
+
// be scrolled up to show it above the FAB.
|
|
19
|
+
import { useId, type MouseEvent, type ReactNode } from 'react';
|
|
18
20
|
import { createPortal } from 'react-dom';
|
|
19
21
|
import { ArrowLeft, Bell, CaretRight, ArrowSquareOut } from '@phosphor-icons/react';
|
|
20
22
|
import { t } from './i18n';
|
|
21
23
|
import { IconButton, IconLink, MenuButton, TabPanel, TabRow, TopAppBar, useMenuLayout, type TabChoice } from './ui';
|
|
22
24
|
import { up } from './nav';
|
|
23
25
|
import EditorFooter from './EditorFooter';
|
|
24
|
-
import useFooterClearance from './useFooterClearance';
|
|
25
26
|
import { FabPresenceContext, useCountFabs } from './fabPresence';
|
|
26
27
|
import { useMenuOpen } from './menuOpen';
|
|
27
28
|
import { unseen, useNotifications } from './notificationData';
|
|
@@ -120,9 +121,7 @@ export default function Screen<T extends string>({
|
|
|
120
121
|
else way();
|
|
121
122
|
};
|
|
122
123
|
const slot = useTopBarSlot();
|
|
123
|
-
const main = useRef<HTMLElement>(null);
|
|
124
124
|
const fabs = useCountFabs();
|
|
125
|
-
useFooterClearance(main, fabs.count > 0);
|
|
126
125
|
// The way here, without the start screen: the logo in the bar is the way there.
|
|
127
126
|
const crumbs = back && back.href !== '#' ? [back] : [];
|
|
128
127
|
const bar = (
|
|
@@ -175,12 +174,12 @@ export default function Screen<T extends string>({
|
|
|
175
174
|
</TopAppBar>
|
|
176
175
|
);
|
|
177
176
|
return (
|
|
178
|
-
<div className={`flex flex-col ${slot ? 'flex-1' : 'min-h-
|
|
177
|
+
<div className={`flex flex-col ${slot ? 'flex-1' : 'min-h-[calc(100dvh+var(--editor-fab-clearance,0px))]'}`}>
|
|
179
178
|
{slot ? createPortal(bar, slot) : bar}
|
|
180
179
|
{/* A row, so the column keeps its margins (editor.css .editor-column) and is as high as the rest of the window. */}
|
|
181
180
|
<div className="flex flex-1">
|
|
182
181
|
<main
|
|
183
|
-
|
|
182
|
+
data-clearance={fabs.count > 0}
|
|
184
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}`}
|
|
185
184
|
>
|
|
186
185
|
{/* At expanded and wider: the way here, the title and the status, under the bar. */}
|
package/src/editor.css
CHANGED
|
@@ -419,6 +419,15 @@
|
|
|
419
419
|
z-index: 30;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
/* Below medium, while a screen leaves room for its FAB under the footer (Screen.tsx, data-clearance), the page is that room
|
|
423
|
+
taller than the window (the screen's padding less the bare one), so a short screen still has its footer at the bottom of
|
|
424
|
+
the window and can be scrolled up to show it above the FAB. */
|
|
425
|
+
@media (width < theme(--breakpoint-medium)) {
|
|
426
|
+
:root:has(.editor-screen[data-clearance='true']) {
|
|
427
|
+
--editor-fab-clearance: 4.5rem;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
422
431
|
/* A FAB whose action runs or cannot be used now (FabAction.busy, .disabled): dimmed, and nothing to press. */
|
|
423
432
|
.editor-fab :is(.extended-fab, .fab):disabled {
|
|
424
433
|
opacity: 0.38;
|
package/src/fabPresence.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Lets a screen know it shows a FAB: Screen (Screen.tsx) provides the context, FabMenu (ui.tsx) calls useRegisterFab while
|
|
2
2
|
// it is drawn. Exposes FabPresenceContext, useCountFabs and useRegisterFab. Used to leave room under the footer for the FAB
|
|
3
|
-
// only (
|
|
3
|
+
// only (Screen.tsx).
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
/** Called with +1 when a FAB appears on the screen and -1 when it goes. Absent outside a Screen. */
|
package/src/topBarSlot.tsx
CHANGED
|
@@ -19,8 +19,9 @@ export function TopBarLayout({ above, aside, children }: { above?: ReactNode; as
|
|
|
19
19
|
{above}
|
|
20
20
|
<div className="flex">
|
|
21
21
|
{aside}
|
|
22
|
-
{/* At least as high as the window, so a short screen's footer is at its bottom (Screen.tsx)
|
|
23
|
-
|
|
22
|
+
{/* At least as high as the window, so a short screen's footer is at its bottom (Screen.tsx), and the room for a FAB
|
|
23
|
+
under the footer higher (--editor-fab-clearance, editor.css). */}
|
|
24
|
+
<div className="flex min-h-[calc(100dvh+var(--editor-fab-clearance,0px))] min-w-0 flex-1 flex-col">
|
|
24
25
|
<div ref={setSlot} className="editor-top-bar-slot" />
|
|
25
26
|
<Slot.Provider value={slot}>{slot && children}</Slot.Provider>
|
|
26
27
|
</div>
|
package/starter/package.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@material-symbols/svg-400": "0.48.2",
|
|
33
33
|
"@material/material-color-utilities": "^0.3.0",
|
|
34
34
|
"@milkdown/kit": "^7.22.3",
|
|
35
|
-
"@morphemecms/cms": "0.14.
|
|
35
|
+
"@morphemecms/cms": "0.14.2",
|
|
36
36
|
"@phosphor-icons/core": "^2.1.1",
|
|
37
37
|
"@phosphor-icons/react": "^2.1.10",
|
|
38
38
|
"@tailwindcss/typography": "^0.5.20",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function needsClearance(pageHeight: number, windowHeight: number, hasFab: boolean): boolean;
|
package/lib/footerClearance.mjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Whether a screen needs clearance under its footer for the floating action button: only when it has one and its content
|
|
2
|
-
// pushes the footer below the window, so a short screen has its footer at the very bottom. Exposes needsClearance. No imports.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {number} pageHeight The page's height with no clearance added.
|
|
6
|
-
* @param {number} windowHeight The window's height.
|
|
7
|
-
* @param {boolean} hasFab Whether the screen shows a FAB.
|
|
8
|
-
*/
|
|
9
|
-
export const needsClearance = (pageHeight, windowHeight, hasFab) => hasFab && pageHeight > windowHeight;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Tests for lib/footerClearance.mjs: clearance under the footer only for a FAB, and only when the page is taller than the window.
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { needsClearance } from './footerClearance.mjs';
|
|
5
|
-
|
|
6
|
-
test('a page that fits the window needs no clearance', () => {
|
|
7
|
-
assert.equal(needsClearance(600, 800, true), false);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test('a page exactly as tall as the window needs none', () => {
|
|
11
|
-
assert.equal(needsClearance(800, 800, true), false);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('a tall page with a FAB needs clearance', () => {
|
|
15
|
-
assert.equal(needsClearance(801, 800, true), true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('a tall page without a FAB needs none', () => {
|
|
19
|
-
assert.equal(needsClearance(2000, 800, false), false);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('a short page without a FAB needs none', () => {
|
|
23
|
-
assert.equal(needsClearance(600, 800, false), false);
|
|
24
|
-
});
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "update-card-whats-new",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "Nyhederne møder dig på startskærmen",
|
|
5
|
-
"text": "Efter en opdatering skal du ikke selv lede efter, hvad der er nyt. Øverst på startskærmen ser du nu den nye version og de vigtigste nyheder, med et billede af den største. Tryk på Se, hvad der er nyt for at se det hele."
|
|
6
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "update-card-whats-new",
|
|
3
|
-
"kind": "card",
|
|
4
|
-
"title": "What's new meets you on the start screen",
|
|
5
|
-
"text": "After an update, you shouldn't have to look for what's new yourself. The top of the start screen now shows the new version and the main news, with a picture of the biggest one. Tap See what's new to see it all."
|
|
6
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Gives a screen's <main> data-clearance="true" while its content pushes the footer below the window (so the floating action
|
|
2
|
-
// button, if the screen has one, has room under it) and "false" while the footer sits at the bottom of the window. Exposes useFooterClearance. It
|
|
3
|
-
// measures the page with the clearance off, so the answer cannot flip back and forth; the decision is needsClearance in
|
|
4
|
-
// lib/footerClearance.mjs. The attribute is set on the element, with no re-render.
|
|
5
|
-
import { useLayoutEffect, type RefObject } from 'react';
|
|
6
|
-
import { needsClearance } from '../lib/footerClearance.mjs';
|
|
7
|
-
|
|
8
|
-
export default function useFooterClearance(ref: RefObject<HTMLElement | null>, hasFab: boolean) {
|
|
9
|
-
useLayoutEffect(() => {
|
|
10
|
-
const main = ref.current;
|
|
11
|
-
if (!main) return;
|
|
12
|
-
// The page's layout height (offsetHeight), not scrollHeight: that counts a screen's entrance animation, which moves it a few px.
|
|
13
|
-
const measure = () => {
|
|
14
|
-
main.dataset.clearance = 'false';
|
|
15
|
-
const root = document.documentElement;
|
|
16
|
-
main.dataset.clearance = String(needsClearance(document.body.offsetHeight, root.clientHeight, hasFab));
|
|
17
|
-
};
|
|
18
|
-
measure();
|
|
19
|
-
const observer = new ResizeObserver(measure);
|
|
20
|
-
// Watch the parts inside <main> and what lies beside it and its ancestors (the top bar), not <main> itself: their
|
|
21
|
-
// size does not depend on the clearance, so a change in them is a real change of the page.
|
|
22
|
-
for (let el: Element | null = main; el && el !== document.documentElement; el = el.parentElement)
|
|
23
|
-
for (const part of el === main ? el.children : (el.parentElement?.children ?? [])) observer.observe(part);
|
|
24
|
-
window.addEventListener('resize', measure);
|
|
25
|
-
return () => {
|
|
26
|
-
observer.disconnect();
|
|
27
|
-
window.removeEventListener('resize', measure);
|
|
28
|
-
};
|
|
29
|
-
}, [ref, hasFab]);
|
|
30
|
-
}
|
|
File without changes
|
|
File without changes
|