@morphemecms/cms 0.14.1 → 0.14.2-next-20261107121812

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @morphemecms/cms
2
2
 
3
+ ## 0.14.2-next-20261107121812
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+ - 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
+
3
10
  ## 0.14.1
4
11
 
5
12
  ### Patch Changes
package/lib/colors.ts CHANGED
@@ -43,6 +43,10 @@ function declarations(set: ColorSet): string {
43
43
  .join('');
44
44
  }
45
45
 
46
+ /** The colour of the editor's page in a mode: Surface Container, which Material 3's segmented lists stand on (editor.css),
47
+ * or the background when the colours have no such role. */
48
+ export const pageColor = (set: ColorSet): string => set['surface-container'] ?? set.bg;
49
+
46
50
  /** The colours as CSS for a <style> in the <head>: light mode on :root; dark mode when the phone or
47
51
  * computer is set to it (unless light is picked, data-theme="light" on <html>) and when dark is picked.
48
52
  * Throws on anything that is not a colour, so a broken file never reaches a page. */
@@ -37,6 +37,19 @@ export function imagePath(version: string, locale: string, id: string): string;
37
37
  * @returns {ReleaseNote[] | undefined}
38
38
  */
39
39
  export function notesFor(notes: Record<string, ReleaseNote[]> | undefined, locale: string): ReleaseNote[] | undefined;
40
+ /**
41
+ * The notes that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
42
+ * it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
43
+ * own version yet. Nothing when `from` is not a version.
44
+ * @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
45
+ * @param {string | undefined} from
46
+ * @param {string} locale
47
+ * @returns {ReleaseNote[]}
48
+ */
49
+ export function notesSince(entries: {
50
+ version: string;
51
+ notes?: Record<string, ReleaseNote[]>;
52
+ }[], from: string | undefined, locale: string): ReleaseNote[];
40
53
  /**
41
54
  * The notes of every released version in the running minor line (x.y.0 up to `version`), from the files the package carries
42
55
  * (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address of its screenshot
@@ -1,7 +1,7 @@
1
1
  // Pure helpers for release notes: checks pending notes (cards with a screenshot of the right type and size, lines), groups them per version and locale,
2
- // picks a locale's notes, builds the notes of the running minor line from the package files and reads a version's notes and screenshots from its tar.
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
- // Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor, bundledNotes, staleImageDirs, notesFromTar, imageUrl, imageKey,
4
+ // Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor, notesSince, bundledNotes, staleImageDirs, notesFromTar, imageUrl, imageKey,
5
5
  // parseImagePath and withImageUrls. 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';
@@ -155,6 +155,25 @@ export function notesFor(notes, locale) {
155
155
  }
156
156
 
157
157
  const PLAIN = /^\d+\.\d+\.\d+$/;
158
+
159
+ /**
160
+ * The notes that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
161
+ * it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
162
+ * own version yet. Nothing when `from` is not a version.
163
+ * @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
164
+ * @param {string | undefined} from
165
+ * @param {string} locale
166
+ * @returns {ReleaseNote[]}
167
+ */
168
+ export function notesSince(entries, from, locale) {
169
+ const [base, pre] = String(from ?? '').split('-');
170
+ if (!PLAIN.test(base)) return [];
171
+ const notes = entries
172
+ .filter((entry) => PLAIN.test(entry.version) && compareVersions(entry.version, base) >= (pre === undefined ? 1 : 0))
173
+ .sort((a, b) => compareVersions(b.version, a.version))
174
+ .flatMap((entry) => notesFor(entry.notes, locale) ?? []);
175
+ return [...notes.filter((note) => note.kind === 'card'), ...notes.filter((note) => note.kind === 'line')];
176
+ }
158
177
  const sameMinor = (a, b) => a.split('.').slice(0, 2).join('.') === b.split('.').slice(0, 2).join('.');
159
178
 
160
179
  /**
@@ -11,6 +11,7 @@ import {
11
11
  released,
12
12
  imagePath,
13
13
  notesFor,
14
+ notesSince,
14
15
  bundledNotes,
15
16
  staleImageDirs,
16
17
  notesFromTar,
@@ -164,6 +165,23 @@ test('notesFor falls back to the en-US notes', () => {
164
165
  });
165
166
  test('notesFor gives undefined without notes', () => assert.equal(notesFor(undefined, 'da-DK'), undefined));
166
167
 
168
+ const shipped = (version, notes) => ({ version, text: '', notes: { 'en-US': notes } });
169
+ const ids = (notes) => notes.map((note) => note.id);
170
+
171
+ test('notesSince gives the notes of the newer versions, newest first, cards before lines', () => {
172
+ const entries = [shipped('0.14.0', [card('a'), line('b')]), shipped('0.14.2', [line('c'), card('d')]), shipped('0.14.1', [line('e')])];
173
+ assert.deepEqual(ids(notesSince(entries, '0.14.0', 'da-DK')), ['d', 'c', 'e']);
174
+ assert.deepEqual(ids(notesSince(entries, '0.13.4', 'en-US')), ['d', 'a', 'c', 'e', 'b']);
175
+ assert.deepEqual(notesSince(entries, '0.14.2', 'en-US'), []);
176
+ });
177
+ test('notesSince counts the version of a test version as new', () => {
178
+ assert.deepEqual(ids(notesSince([shipped('0.14.1', [line('a')])], '0.14.1-next.2', 'en-US')), ['a']);
179
+ });
180
+ test('notesSince gives nothing without a version seen', () => {
181
+ assert.deepEqual(notesSince([shipped('0.14.1', [line('a')])], undefined, 'en-US'), []);
182
+ assert.deepEqual(notesSince([shipped('0.14.1', [line('a')])], 'latest', 'en-US'), []);
183
+ });
184
+
167
185
  const file = (version, notes) => ({ version, notes });
168
186
  const dir = '../release-notes/i18n';
169
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.14.1",
3
+ "version": "0.14.2-next-20261107121812",
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",
@@ -29,6 +29,9 @@ import { colorRoles } from '@site/colors';
29
29
  // "Bevægelse" in its place at once (packages/editor/src/theme.ts).
30
30
  import { motionCss } from '@morphemecms/cms/lib/motion';
31
31
  import { motionScheme } from '@morphemecms/cms/site/motion';
32
+ import { pageColor } from '@morphemecms/cms/lib/colors';
33
+
34
+ const shown = completeColors(colors, colorRoles);
32
35
  ---
33
36
 
34
37
  <!doctype html>
@@ -48,11 +51,11 @@ import { motionScheme } from '@morphemecms/cms/site/motion';
48
51
  <meta name="apple-mobile-web-app-capable" content="yes" />
49
52
  <meta name="apple-mobile-web-app-title" content={config.name} />
50
53
  <meta name="apple-mobile-web-app-status-bar-style" content="default" />
51
- <!-- The top bar in the background's colour, the screens' own and the app bar's at the top; data-color: that of its
52
- own mode, for a picked one. -->
53
- <meta name="theme-color" content={colors.light.bg} data-color={colors.light.bg} media="(prefers-color-scheme: light)" />
54
- <meta name="theme-color" content={colors.dark.bg} data-color={colors.dark.bg} media="(prefers-color-scheme: dark)" />
55
- <style id="site-colors" is:inline set:html={colorsCss(completeColors(colors, colorRoles))}></style>
54
+ <!-- The top bar in the editor's page colour (pageColor), the screens' own and the app bar's at the top; data-color: that
55
+ of its own mode, for a picked one. -->
56
+ <meta name="theme-color" content={pageColor(shown.light)} data-color={pageColor(shown.light)} media="(prefers-color-scheme: light)" />
57
+ <meta name="theme-color" content={pageColor(shown.dark)} data-color={pageColor(shown.dark)} media="(prefers-color-scheme: dark)" />
58
+ <style id="site-colors" is:inline set:html={colorsCss(shown)}></style>
56
59
  <style id="site-motion" is:inline set:html={motionCss(motionScheme)}></style>
57
60
  <!-- Light or dark as picked on "Konto" (packages/editor/src/theme.ts), before the page is drawn. The
58
61
  language (lang) is the site's until the editor has picked its own (packages/editor/src/i18n/). -->
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "id": "quiet-lamps-wander",
44
44
  "kind": "card",
45
- "title": "Se, hvad der er nyt, med billeder",
46
- "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."
45
+ "title": "Se, hvad der er nyt",
46
+ "text": "Det kan være svært at følge med i, hvad der er nyt i Morpheme. Derfor får du nu et kort overblik over de nye funktioner efter hver opdatering. Du finder det hele samlet på siden Morpheme."
47
47
  }
48
48
  ]
49
49
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "lists-follow-md3",
3
+ "kind": "line",
4
+ "text": "Lister i editoren viser nu lyse rækker på en tonet side, som i Material 3."
5
+ }
@@ -0,0 +1,6 @@
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
+ }
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "id": "quiet-lamps-wander",
44
44
  "kind": "card",
45
- "title": "See what's new, with pictures",
46
- "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."
45
+ "title": "See what's new",
46
+ "text": "It can be hard to keep up with what's new in Morpheme. So after each update, you now get a short overview of the new features. You'll find it all in one place on the Morpheme page."
47
47
  }
48
48
  ]
49
49
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "lists-follow-md3",
3
+ "kind": "line",
4
+ "text": "Lists in the editor now show light rows on a tinted page, as in Material 3."
5
+ }
@@ -0,0 +1,6 @@
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
+ }
package/src/CmsPage.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  // "Morpheme" (#morpheme), under "Indstillinger": the CMS version the site runs, the notes of its minor line (from the
2
2
  // package itself, bundledNotes.ts), a newer one with its notes, why it is not taken by itself, and an update's progress, and the site's connection to the
3
3
  // Morpheme store (MorphemeConnection) as its last group. Exposes CMS_ROUTE and the page. Depends on cmsView.mjs, bundledNotes.ts,
4
- // ReleaseNotes (the notes as cards), MorphemeConnection and the Worker's /api/cms.
4
+ // ReleaseNotes (the notes as an article), MorphemeConnection and the Worker's /api/cms.
5
5
  import { useCallback, useEffect, useState } from 'react';
6
6
  import { CheckCircle, Clock, Info, Sparkle, WarningCircle } from '@phosphor-icons/react';
7
7
  import Screen from './Screen';
@@ -1,9 +1,8 @@
1
- // A titled list of versions with their release notes on the Morpheme page (#morpheme): per version a small heading, its
2
- // cards (screenshot, title, text) in the editor's language (else English), then its other changes as a short list under
3
- // "Also in this version"; for a version without notes, one row with its text. Exposes ReleaseNotes and VersionNotes.
4
- // Depends on ReleaseNoteCard, ui.tsx, lib/releaseNotes.mjs (notesFor) and i18n.
5
- import { ReleaseNoteCard } from './ReleaseNoteCard';
6
- import { GroupTitle, Group, ListItem } from './ui';
1
+ // The release notes on the Morpheme page (#morpheme), as an article: its title, then per version a small heading, its
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
+ // under "Also in this version"; for a version without notes, its text. No cards and no screenshots: the start screen's
4
+ // update card (UpdatesNotice.tsx) shows the pictures. Exposes ReleaseNotes and VersionNotes. Depends on
5
+ // lib/releaseNotes.mjs (notesFor) and i18n.
7
6
  import { locale, t } from './i18n';
8
7
  import { notesFor } from '../lib/releaseNotes.mjs';
9
8
  import type { ReleaseNote } from '../lib/releaseNotes.mjs';
@@ -13,40 +12,40 @@ export type VersionNotes = { version: string; text: string; notes?: Record<strin
13
12
  type Card = Extract<ReleaseNote, { kind: 'card' }>;
14
13
 
15
14
  function VersionEntry({ entry }: { entry: VersionNotes }) {
16
- const notes = notesFor(entry.notes, locale);
17
- if (!notes?.length) {
18
- return (
19
- <Group>
20
- <ListItem headline={t.cms.running(entry.version)} supporting={entry.text} />
21
- </Group>
22
- );
23
- }
15
+ const notes = notesFor(entry.notes, locale) ?? [];
24
16
  const cards = notes.filter((note): note is Card => note.kind === 'card');
25
17
  const lines = notes.filter((note) => note.kind === 'line');
26
18
  return (
27
- <div className="mb-6 space-y-3">
28
- <h3 className="px-4 text-sm font-medium text-muted">{t.cms.running(entry.version)}</h3>
19
+ <section className="space-y-5 border-t border-line pt-5 first-of-type:border-t-0 first-of-type:pt-0">
20
+ <h3 className="text-sm font-medium text-muted">{t.cms.running(entry.version)}</h3>
21
+ {!notes.length && entry.text && <p className="text-base">{entry.text}</p>}
29
22
  {cards.map((note) => (
30
- <ReleaseNoteCard key={note.id} note={note} />
23
+ <div key={note.id} className="space-y-1">
24
+ <h4 className="text-lg font-medium">{note.title}</h4>
25
+ <p className="text-base">{note.text}</p>
26
+ </div>
31
27
  ))}
32
28
  {lines.length > 0 && (
33
- <Group title={t.cms.alsoInThisVersion}>
34
- {lines.map((note) => (
35
- <ListItem key={note.id} headline={note.text} />
36
- ))}
37
- </Group>
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>
38
37
  )}
39
- </div>
38
+ </section>
40
39
  );
41
40
  }
42
41
 
43
42
  export function ReleaseNotes({ title, entries }: { title: string; entries: VersionNotes[] }) {
44
43
  return (
45
- <section className="mb-8">
46
- <GroupTitle>{title}</GroupTitle>
44
+ <article className="mb-8 max-w-prose space-y-5 px-4 not-first:mt-8">
45
+ <h2 className="text-2xl">{title}</h2>
47
46
  {entries.map((entry) => (
48
47
  <VersionEntry key={entry.version} entry={entry} />
49
48
  ))}
50
- </section>
49
+ </article>
51
50
  );
52
51
  }
@@ -1,16 +1,28 @@
1
- // At the top of the start screen: what was updated since the last visit in this browser (the CMS and the apps installed
2
- // from the store), with a close button that marks it seen. Exposes UpdatesNotice. The logic is startView.mjs's
3
- // (changesSince), the storage startStorage.ts's, the card the theme's (Notice in ui.tsx).
1
+ // At the top of the start screen: what was updated since the last visit in this browser, as a card with a close button
2
+ // that marks it seen, in the accent (the editor's blue). For the CMS: its new version large, how many new things it brings and the first of them from the
3
+ // notes the package carries (bundledNotes.ts: the first with a screenshot large, then the titles and texts of the next),
4
+ // and a button to the Morpheme page with all of them; for each app installed from the store: a row with its icon and new
5
+ // version, to its Information tab. Exposes UpdatesNotice. The logic is startView.mjs's (changesSince) and
6
+ // releaseNotes.mjs's (notesSince), the storage startStorage.ts's, the card, rows and buttons the theme's (themeUi.ts, ui.tsx).
4
7
  import { useEffect, useState } from 'react';
8
+ import { ArrowRight, Sparkle } from '@phosphor-icons/react';
5
9
  import { version } from '../package.json';
6
- import { t } from './i18n';
10
+ import { locale, t } from './i18n';
7
11
  import { installedApps } from './apps';
8
12
  import { apps } from './site';
9
13
  import { changesSince, versioned, type Versions } from './startView.mjs';
10
14
  import { keepStored, SEEN, storedSeen } from './startStorage';
11
15
  import type { RepoFile } from './github';
12
- import { Notice } from './ui';
13
- import { UpdateLink } from './UpdateLink';
16
+ import { SiteCard } from './themeUi';
17
+ import { CloseButton, LinkButton, ListItem, plainListClass } from './ui';
18
+ import { CMS_ROUTE } from './CmsPage';
19
+ import { appHref } from './AppList';
20
+ import { runningNotes } from './bundledNotes';
21
+ import { notesSince, type ReleaseNote } from '../lib/releaseNotes.mjs';
22
+ import { INFO_KEY } from '../lib/appRoute.mjs';
23
+
24
+ /** How many of the new things the card shows; the Morpheme page has them all. */
25
+ const SHOWN = 3;
14
26
 
15
27
  /** The CMS's version and those of the apps installed from the store, as the editor is built with them now. */
16
28
  function versionsNow(files: RepoFile[]): Versions {
@@ -21,8 +33,29 @@ function versionsNow(files: RepoFile[]): Versions {
21
33
  };
22
34
  }
23
35
 
24
- /** Each change is a link to what is new in it (the CMS's page, an app's Information tab). The first visit, and a visit
25
- * with nothing new, only notes the versions. */
36
+ /** The first new thing with a screenshot, large: the screenshot fills the top (its top part), its title under it. */
37
+ function Lead({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
38
+ return (
39
+ <div className="overflow-hidden rounded-xl bg-accent-container text-on-accent-container">
40
+ <img src={note.image} alt="" className="block h-44 w-full object-cover object-top" />
41
+ <p className="px-4 py-3 font-medium">{note.title}</p>
42
+ </div>
43
+ );
44
+ }
45
+
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
+ /** The first visit, and a visit with nothing new, only notes the versions. */
26
59
  export function UpdatesNotice({ files }: { files: RepoFile[] }) {
27
60
  const now = versionsNow(files);
28
61
  const [changes, setChanges] = useState(() => changesSince(storedSeen(), now));
@@ -35,15 +68,66 @@ export function UpdatesNotice({ files }: { files: RepoFile[] }) {
35
68
  keepStored(SEEN, versioned(now));
36
69
  setChanges([]);
37
70
  };
71
+ const cms = changes.find((c) => c.id === 'cms');
72
+ const updatedApps = changes.flatMap((c) => {
73
+ const app = c.id === 'cms' ? null : apps.find((a) => a.id === c.id);
74
+ return app ? [{ app, version: c.version }] : [];
75
+ });
76
+ const news = cms ? notesSince(runningNotes, cms.from, locale) : [];
77
+ const lead = news.find((note): note is Extract<ReleaseNote, { kind: 'card' }> => note.kind === 'card' && !!note.image);
78
+ const rest = news.filter((note) => note !== lead);
38
79
  return (
39
- <div className="mb-8">
40
- <Notice title={t.library.updatesTitle} onClose={close}>
41
- <ul className="space-y-1">
42
- {changes.map((c) => (
43
- <UpdateLink key={c.id} change={c} />
80
+ <SiteCard
81
+ kind="filled"
82
+ as="section"
83
+ role="status"
84
+ aria-labelledby="updates-title"
85
+ className="relative mb-8 text-on-accent [--card-filled-surface:var(--accent)] [--card-padding:0] [--icon-button-standard-text:var(--on-accent)]"
86
+ >
87
+ <CloseButton onClick={close} />
88
+ <div className="flex items-center gap-4 p-4 pe-14">
89
+ <span className="grid size-12 shrink-0 place-items-center rounded-full bg-on-accent text-accent">
90
+ <Sparkle size={26} weight="fill" aria-hidden />
91
+ </span>
92
+ <div className="min-w-0">
93
+ <h2 id="updates-title" className="text-sm font-medium">
94
+ {t.library.updatesTitle}
95
+ </h2>
96
+ <p className="text-2xl leading-tight">{cms ? t.library.cmsVersion(cms.version) : t.library.appsUpdated(updatedApps.length)}</p>
97
+ {news.length > 0 && <p className="text-sm opacity-85">{t.library.newThings(news.length)}</p>}
98
+ </div>
99
+ </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
+ {updatedApps.length > 0 && (
118
+ <ul className={`${plainListClass} mx-4 mb-4 overflow-hidden rounded-xl bg-accent-container text-on-accent-container`}>
119
+ {updatedApps.map(({ app, version }) => (
120
+ <ListItem
121
+ key={app.id}
122
+ href={`${appHref(app)}/${INFO_KEY}`}
123
+ icon={app.icon}
124
+ headline={app.label}
125
+ supporting={t.library.appUpdatedTo(version)}
126
+ arrow
127
+ />
44
128
  ))}
45
129
  </ul>
46
- </Notice>
47
- </div>
130
+ )}
131
+ </SiteCard>
48
132
  );
49
133
  }
package/src/editor.css CHANGED
@@ -483,6 +483,17 @@
483
483
  font-weight: var(--type-label-medium-weight);
484
484
  }
485
485
 
486
+ /* Material 3 Expressive's segmented lists (m3.material.io/components/lists/specs, colour role 1): each item on Surface (the
487
+ theme's page colour, --bg) over a Surface Container, as Material's own examples and Android's settings show them. So
488
+ the editor's page, its top app bar and the label of an outlined field (--md-background) are that container, the top
489
+ app bar a step higher once the page scrolls under it, and the items on Surface. html:root, so it wins over the theme's
490
+ :root. */
491
+ html:root {
492
+ --md-background: var(--surface-container, var(--bg));
493
+ --top-app-bar-stuck-surface: var(--surface-container-high, var(--elevation-3-surface));
494
+ --list-segmented-surface: var(--bg);
495
+ }
496
+
486
497
  /* The content column (.editor-column: Screen.tsx's tabs row and its main) stands centred on the window, not in the space
487
498
  beside the panels. --column-start and --column-end are the widths of the panels beside the content: the navigation
488
499
  rail from medium, the standard drawer instead of it from expanded while it is open (:root[data-sidebar='open']; the
@@ -9,8 +9,10 @@ export const library: Messages['library'] = {
9
9
  continueEmpty: 'De sider og indlæg, du redigerer, vises her.',
10
10
  recentEdit: (kind, when) => `${kind} · Redigeret ${when}`,
11
11
  updatesTitle: 'Opdateret siden dit sidste besøg',
12
- cmsName: 'Morpheme',
13
- updatedTo: (name, version) => `${name} er opdateret til ${version}`,
12
+ appsUpdated: (count) => (count === 1 ? '1 app er opdateret' : `${count} apps er opdateret`),
13
+ newThings: (count) => (count === 1 ? '1 nyhed' : `${count} nyheder`),
14
+ seeWhatsNew: 'Se, hvad der er nyt',
15
+ appUpdatedTo: (version) => `Opdateret til ${version}`,
14
16
  githubAccount: (login) => `GitHub-konto: ${login}`,
15
17
  logOut: 'Log ud',
16
18
  notifications: 'Notifikationer',
@@ -14,12 +14,16 @@ export const library = {
14
14
  continueEmpty: 'The pages and posts you edit show up here.',
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
- /** At the top of the start screen: what was updated since the last visit in this browser, each a link to what's new. */
17
+ /** At the top of the start screen (UpdatesNotice.tsx): the card with what was updated since the last visit in this
18
+ * browser. Its title, over the CMS's new version (cmsVersion) or, with only apps updated, how many. */
18
19
  updatesTitle: 'Updated since your last visit',
19
- /** The CMS's name in that notice. */
20
- cmsName: 'Morpheme',
21
- /** The CMS or an app with its new version. */
22
- updatedTo: (name: string, version: string) => `${name} is updated to ${version}`,
20
+ appsUpdated: (count: number) => (count === 1 ? '1 app updated' : `${count} apps updated`),
21
+ /** Under the CMS's new version: how many new things its release notes have since the version seen last. */
22
+ newThings: (count: number) => (count === 1 ? '1 new thing' : `${count} new things`),
23
+ /** The button to the Morpheme page, with all the release notes. */
24
+ seeWhatsNew: 'See what’s new',
25
+ /** Under an updated app's name. */
26
+ appUpdatedTo: (version: string) => `Updated to ${version}`,
23
27
  githubAccount: (login: string) => `GitHub account: ${login}`,
24
28
  logOut: 'Log out',
25
29
  /** The bell in the top bar and its side sheet (Notifications.tsx): the messages shown at the bottom. */
@@ -42,15 +42,16 @@ export function continueShown(closedAt: unknown, latestAt: number | undefined):
42
42
  */
43
43
  /**
44
44
  * What has a version other than at the last visit: the CMS (`id` "cms") and each app installed then and now, with its
45
- * version now. Nothing at the first visit (`seen` null): there is nothing to compare with. An app installed since is
45
+ * version now and the one seen (`from`). Nothing at the first visit (`seen` null): there is nothing to compare with. An app installed since is
46
46
  * not an update.
47
47
  * @param {Versions | null} seen
48
48
  * @param {Versions} now
49
- * @returns {{ id: string, version: string }[]}
49
+ * @returns {{ id: string, version: string, from: string }[]}
50
50
  */
51
51
  export function changesSince(seen: Versions | null, now: Versions): {
52
52
  id: string;
53
53
  version: string;
54
+ from: string;
54
55
  }[];
55
56
  /**
56
57
  * What the editor's start screen (Library.tsx Start, ContinueCard.tsx, UpdatesNotice.tsx) shows besides its FAB: the pages and blog posts
package/src/startView.mjs CHANGED
@@ -90,19 +90,19 @@ export function continueShown(closedAt, latestAt) {
90
90
 
91
91
  /**
92
92
  * What has a version other than at the last visit: the CMS (`id` "cms") and each app installed then and now, with its
93
- * version now. Nothing at the first visit (`seen` null): there is nothing to compare with. An app installed since is
93
+ * version now and the one seen (`from`). Nothing at the first visit (`seen` null): there is nothing to compare with. An app installed since is
94
94
  * not an update.
95
95
  * @param {Versions | null} seen
96
96
  * @param {Versions} now
97
- * @returns {{ id: string, version: string }[]}
97
+ * @returns {{ id: string, version: string, from: string }[]}
98
98
  */
99
99
  export function changesSince(seen, now) {
100
100
  if (!seen) return [];
101
101
  const changed = [];
102
- if (seen.cms && now.cms && seen.cms !== now.cms) changed.push({ id: 'cms', version: now.cms });
102
+ if (seen.cms && now.cms && seen.cms !== now.cms) changed.push({ id: 'cms', version: now.cms, from: seen.cms });
103
103
  for (const [id, version] of Object.entries(now.apps)) {
104
104
  const before = seen.apps?.[id];
105
- if (before && version && before !== version) changed.push({ id, version });
105
+ if (before && version && before !== version) changed.push({ id, version, from: before });
106
106
  }
107
107
  return changed;
108
108
  }
@@ -38,8 +38,8 @@ test('finds the CMS and the apps with another version', () => {
38
38
  const seen = { cms: '1.0.0', apps: { a: '1.0.0', b: '2.0.0' } };
39
39
  const now = { cms: '1.1.0', apps: { a: '1.0.0', b: '2.1.0' } };
40
40
  assert.deepEqual(changesSince(seen, now), [
41
- { id: 'cms', version: '1.1.0' },
42
- { id: 'b', version: '2.1.0' },
41
+ { id: 'cms', version: '1.1.0', from: '1.0.0' },
42
+ { id: 'b', version: '2.1.0', from: '2.0.0' },
43
43
  ]);
44
44
  });
45
45
 
@@ -107,8 +107,8 @@ test('stored data is read only with the current version', () => {
107
107
 
108
108
  test('a version that went down counts as a change (current behaviour)', () => {
109
109
  assert.deepEqual(changesSince({ cms: '1.1.0', apps: { a: '2.0.0' } }, { cms: '1.0.0', apps: { a: '1.0.0' } }), [
110
- { id: 'cms', version: '1.0.0' },
111
- { id: 'a', version: '1.0.0' },
110
+ { id: 'cms', version: '1.0.0', from: '1.1.0' },
111
+ { id: 'a', version: '1.0.0', from: '2.0.0' },
112
112
  ]);
113
113
  });
114
114
 
package/src/theme.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // (the theme's styles/global.css); the script in the head of packages/editor/pages/editor/index.astro does the same
4
4
  // before the page is drawn, so it does not flash in the other colours. Its own key, so the site's
5
5
  // choice (`theme`) is not changed by the editor's.
6
- import { colorsCss, isColor, type Colors } from '../lib/colors';
6
+ import { colorsCss, isColor, pageColor, type Colors } from '../lib/colors';
7
7
  import { motionCss, type MotionScheme } from '../lib/motion';
8
8
 
9
9
  export type Theme = 'light' | 'auto' | 'dark';
@@ -73,7 +73,7 @@ export function applyColors(colors: Colors) {
73
73
  const style = document.getElementById('site-colors');
74
74
  if (style && style.textContent !== css) style.textContent = css;
75
75
  for (const m of bars()) {
76
- const bg = colors[isDark(m) ? 'dark' : 'light'].bg;
76
+ const bg = pageColor(colors[isDark(m) ? 'dark' : 'light']);
77
77
  if (isColor(bg)) m.dataset.color = bg;
78
78
  }
79
79
  applyTheme(savedTheme());
@@ -199,14 +199,15 @@
199
199
  --card-filled-surface: var(--md-surface-container-highest);
200
200
  --card-outlined-surface: var(--surface);
201
201
  --card-outlined-line: var(--line);
202
- /* Lists (M3 Expressive's list tokens): items 56, 72 and 88px high by their lines, 16px in from their sides, 10px over
203
- and under their text and 12px between their elements, without lines between them; 20px icons; the headline in body
204
- large, the supporting text in body medium, the overline and the trailing text in label small; a state layer in the
205
- text's colour when pointed at (8%) and pressed (10%), nothing moving aside; disabled at 38%, a segmented item on a
206
- 10% layer of the text's colour. Their corners extra-small, medium pointed at, large pressed and focused, large
207
- increased selected; a selected one in the secondary container. Segmented: each item on a surface container (a step
208
- up from the page, as Material's items on the surface over a container), 2px between them and the list's corners
209
- large. */
202
+ /* Lists (M3 Expressive's list tokens, m3.material.io/components/lists/specs): items 56, 72 and 88px high by their lines,
203
+ 16px in from their sides, 10px over and under their text and 12px between their elements, without lines between
204
+ them; 20px icons; the label text in body large (16/24, 400, 0.5; a setting row's name too), the supporting text in
205
+ body medium, the overline and the trailing text in label small; a state layer in the text's colour when pointed at
206
+ (8%) and pressed (10%), nothing moving aside; disabled at 38%, a segmented item on a 10% layer of the text's colour.
207
+ Their corners extra-small (4px), medium (12px) pointed at, pressed and selected; a selected one in the secondary
208
+ container. Segmented: 2px between the items, the list's outer corners medium (12px), each item on a surface
209
+ container (a step up from the page; the editor puts the items on the surface over a container page, as Material's
210
+ spec does, editor.css). */
210
211
  --list-divider: transparent;
211
212
  --list-item-min-height: 3.5rem;
212
213
  --list-item-two-line-min-height: 4.5rem;
@@ -229,6 +230,7 @@
229
230
  --list-item-selected-surface: var(--secondary-container);
230
231
  --list-item-selected-text: var(--on-secondary-container);
231
232
  --list-item-selected-weight: 400;
233
+ --setting-row-label-weight: var(--type-body-large-weight);
232
234
  --list-item-leading-icon-selected-color: var(--accent);
233
235
  --list-item-disabled-opacity: 0.38;
234
236
  --list-segmented-gap: 0.125rem;
@@ -4418,7 +4418,8 @@
4418
4418
  */
4419
4419
  @layer components {
4420
4420
  /* A setting's row (SettingRow.tsx): the name and the line under it, then what sets it, on one line; the switch's
4421
- and the editor's dropdown row both. */
4421
+ and the editor's dropdown row both. The name in a list item's label type (--list-item-headline-*), so a row with a
4422
+ switch reads as the list rows around it. */
4422
4423
  .setting-row {
4423
4424
  display: flex;
4424
4425
  align-items: center;
@@ -4430,7 +4431,10 @@
4430
4431
  }
4431
4432
  .setting-row-label {
4432
4433
  display: block;
4434
+ font-size: var(--list-item-headline-size, var(--type-body-large-size));
4435
+ line-height: var(--list-item-headline-line-height, var(--type-body-large-line-height));
4433
4436
  font-weight: var(--setting-row-label-weight, var(--switch-label-weight, 500));
4437
+ letter-spacing: var(--list-item-headline-letter-spacing, var(--type-body-large-tracking));
4434
4438
  }
4435
4439
  .setting-row-supporting {
4436
4440
  display: block;
@@ -1,17 +0,0 @@
1
- // One release note card on the Morpheme page (#morpheme): the theme's outlined card with the note's screenshot on top (when it
2
- // has one, whole at the card's width and its own height), its title and its text. Exposes ReleaseNoteCard. Depends on the theme's card (themeUi.ts) and the ReleaseNote
3
- // type of lib/releaseNotes.mjs.
4
- import { SiteCard } from './themeUi';
5
- import type { ReleaseNote } from '../lib/releaseNotes.mjs';
6
-
7
- export function ReleaseNoteCard({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
8
- return (
9
- <SiteCard kind="outlined" as="article" className="space-y-3 overflow-hidden [--card-padding:0_0_1rem]">
10
- {note.image && <img src={note.image} alt={note.title} loading="lazy" className="block h-auto w-full" />}
11
- <div className="space-y-2 px-4">
12
- <h3 className="text-lg font-medium">{note.title}</h3>
13
- <p className="text-base">{note.text}</p>
14
- </div>
15
- </SiteCard>
16
- );
17
- }
@@ -1,21 +0,0 @@
1
- // One line of the start screen's updates notice: a link to what is new in the CMS or in an app (its Information tab).
2
- // Exposes UpdateLink. Depends on appHref (AppList.tsx), CMS_ROUTE and INFO_KEY.
3
- import { t } from './i18n';
4
- import { CMS_ROUTE } from './CmsPage';
5
- import { appHref } from './AppList';
6
- import { apps } from './site';
7
- import { INFO_KEY } from '../lib/appRoute.mjs';
8
-
9
- /** `change.id` is "cms" or an app's id; `change.version` the version it is updated to. */
10
- export function UpdateLink({ change }: { change: { id: string; version: string } }) {
11
- const app = change.id === 'cms' ? null : apps.find((a) => a.id === change.id);
12
- const name = app ? app.label : t.library.cmsName;
13
- const href = app ? `${appHref(app)}/${INFO_KEY}` : `#${CMS_ROUTE}`;
14
- return (
15
- <li>
16
- <a href={href} className="underline underline-offset-2 hover:text-accent">
17
- {t.library.updatedTo(name, change.version)}
18
- </a>
19
- </li>
20
- );
21
- }