@morphemecms/cms 0.14.2 → 0.14.3-next-20261107160925

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/lib/license-texts/Unsplash.txt +7 -0
  3. package/lib/licenses.d.mts +8 -0
  4. package/lib/licenses.editions.test.mjs +45 -0
  5. package/lib/licenses.mjs +40 -0
  6. package/lib/releaseNotes.d.mts +79 -9
  7. package/lib/releaseNotes.mjs +136 -21
  8. package/lib/releaseNotes.test.mjs +187 -5
  9. package/package.json +1 -1
  10. package/release-notes/editions/0.14.json +12 -0
  11. package/release-notes/editions/0.14.webp +0 -0
  12. package/release-notes/i18n/da-DK/0.14.0.json +12 -11
  13. package/release-notes/i18n/da-DK/0.14.1.json +3 -2
  14. package/release-notes/i18n/da-DK/0.14.2.json +5 -4
  15. package/release-notes/i18n/da-DK/pending/edition-photo-card.json +6 -0
  16. package/release-notes/i18n/da-DK/pending/edition-photo-card.webp +0 -0
  17. package/release-notes/i18n/da-DK/pending/short-notification-buttons.json +1 -0
  18. package/release-notes/i18n/da-DK/pending/version-heading-date-photo.json +5 -0
  19. package/release-notes/i18n/en-US/0.14.0.json +13 -12
  20. package/release-notes/i18n/en-US/0.14.1.json +3 -2
  21. package/release-notes/i18n/en-US/0.14.2.json +5 -4
  22. package/release-notes/i18n/en-US/pending/edition-photo-card.json +6 -0
  23. package/release-notes/i18n/en-US/pending/edition-photo-card.webp +0 -0
  24. package/release-notes/i18n/en-US/pending/short-notification-buttons.json +1 -0
  25. package/release-notes/i18n/en-US/pending/version-heading-date-photo.json +5 -0
  26. package/scripts/release-notes.mjs +2 -1
  27. package/src/App.tsx +4 -1
  28. package/src/CmsPage.tsx +7 -2
  29. package/src/Licenses.tsx +4 -0
  30. package/src/Notifications.tsx +1 -1
  31. package/src/ReleaseNoteLines.tsx +17 -0
  32. package/src/ReleaseNoteSection.tsx +12 -0
  33. package/src/ReleaseNotes.tsx +49 -27
  34. package/src/Screen.tsx +2 -2
  35. package/src/UpdatesNotice.tsx +104 -69
  36. package/src/VersionLinePage.tsx +42 -0
  37. package/src/bundledNotes.ts +40 -5
  38. package/src/editor.css +4 -1
  39. package/src/i18n/da-DK/cms.ts +4 -0
  40. package/src/i18n/da-DK/library.ts +7 -4
  41. package/src/i18n/en-US/cms.ts +7 -0
  42. package/src/i18n/en-US/library.ts +12 -5
@@ -0,0 +1,17 @@
1
+ // The smaller changes of a version as a list under "Also in this version", one line each.
2
+ // Exposes ReleaseNoteLines. Depends on i18n and the release note type of lib/releaseNotes.mjs.
3
+ import { t } from './i18n';
4
+ import type { ReleaseNote } from '../lib/releaseNotes.mjs';
5
+
6
+ export function ReleaseNoteLines({ lines }: { lines: ReleaseNote[] }) {
7
+ return (
8
+ <div className="space-y-2">
9
+ <h4 className="text-base font-medium">{t.cms.alsoInThisVersion}</h4>
10
+ <ul className="list-disc space-y-1 ps-5 text-base marker:text-accent">
11
+ {lines.map((note) => (
12
+ <li key={note.id}>{note.text}</li>
13
+ ))}
14
+ </ul>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,12 @@
1
+ // One release note card as a section of the release notes: its title and its text.
2
+ // Exposes ReleaseNoteSection. Depends on the release note type of lib/releaseNotes.mjs.
3
+ import type { ReleaseNote } from '../lib/releaseNotes.mjs';
4
+
5
+ export function ReleaseNoteSection({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
6
+ return (
7
+ <div className="space-y-1">
8
+ <h4 className="text-lg font-medium">{note.title}</h4>
9
+ <p className="text-base">{note.text}</p>
10
+ </div>
11
+ );
12
+ }
@@ -1,50 +1,72 @@
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.
1
+ // The release notes on the Morpheme page (#morpheme), as an article: its title, then per version a heading ("New in
2
+ // Morpheme Braies 0.14.3"), the day it was released and, under a line's newest version, its edition's photo; its cards
3
+ // in the editor's language (else English) as sections with a title and a text, and its other changes as a list under
4
+ // "Also in this version"; for a version without notes, its text. No screenshots: the start screen's update card
5
+ // (UpdatesNotice.tsx) shows them. Exposes ReleaseNotes and VersionNotes; it only composes ReleaseNoteSection.tsx and
6
+ // ReleaseNoteLines.tsx. Depends on lib/releaseNotes.mjs (notesFor) and i18n.
6
7
  import { locale, t } from './i18n';
7
8
  import { notesFor } from '../lib/releaseNotes.mjs';
8
- import type { ReleaseNote } from '../lib/releaseNotes.mjs';
9
+ import type { Edition, ReleaseNote } from '../lib/releaseNotes.mjs';
10
+ import { ReleaseNoteSection } from './ReleaseNoteSection';
11
+ import { ReleaseNoteLines } from './ReleaseNoteLines';
9
12
 
10
- export type VersionNotes = { version: string; text: string; notes?: Record<string, ReleaseNote[]> };
13
+ /** A version's notes; `date` (YYYY-MM-DD) and `edition` for the versions the package carries (bundledNotes.ts). */
14
+ export type VersionNotes = {
15
+ version: string;
16
+ text: string;
17
+ date?: string;
18
+ edition?: Edition;
19
+ notes?: Record<string, ReleaseNote[]>;
20
+ };
21
+
22
+ /** A day (YYYY-MM-DD) as the editor's language writes it ("November 7, 2026"). */
23
+ const dayOf = (date: string) =>
24
+ new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(new Date(`${date}T12:00:00Z`));
11
25
 
12
26
  type Card = Extract<ReleaseNote, { kind: 'card' }>;
13
27
 
14
- function VersionEntry({ entry }: { entry: VersionNotes }) {
28
+ function VersionEntry({ entry, photo }: { entry: VersionNotes; photo: boolean }) {
15
29
  const notes = notesFor(entry.notes, locale) ?? [];
16
30
  const cards = notes.filter((note): note is Card => note.kind === 'card');
17
31
  const lines = notes.filter((note) => note.kind === 'line');
32
+ // A test version (0.14.3-next-…) is named as the version it leads to.
33
+ const version = entry.version.split('-')[0];
34
+ const name = entry.edition ? t.library.cmsEdition(entry.edition.name, version) : t.library.cmsVersion(version);
18
35
  return (
19
36
  <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>
37
+ <div className="space-y-3">
38
+ <div>
39
+ <h3 className="text-xl">{t.cms.newIn(name)}</h3>
40
+ {entry.date && (
41
+ <p className="text-sm text-muted">
42
+ <time dateTime={entry.date}>{dayOf(entry.date)}</time>
43
+ </p>
44
+ )}
45
+ </div>
46
+ {photo && entry.edition && (
47
+ <img
48
+ src={entry.edition.image}
49
+ alt={entry.edition.place[locale] ?? entry.edition.place['en-US'] ?? entry.edition.name}
50
+ className="block aspect-[3/2] w-full rounded-xl object-cover"
51
+ />
52
+ )}
53
+ </div>
21
54
  {!notes.length && entry.text && <p className="text-base">{entry.text}</p>}
22
55
  {cards.map((note) => (
23
- <div key={note.id} className="space-y-1">
24
- <h4 className="text-lg font-medium">{note.title}</h4>
25
- <p className="text-base">{note.text}</p>
26
- </div>
56
+ <ReleaseNoteSection key={note.id} note={note} />
27
57
  ))}
28
- {lines.length > 0 && (
29
- <div className="space-y-2">
30
- <h4 className="text-base font-medium">{t.cms.alsoInThisVersion}</h4>
31
- <ul className="list-disc space-y-1 ps-5 text-base marker:text-accent">
32
- {lines.map((note) => (
33
- <li key={note.id}>{note.text}</li>
34
- ))}
35
- </ul>
36
- </div>
37
- )}
58
+ {lines.length > 0 && <ReleaseNoteLines lines={lines} />}
38
59
  </section>
39
60
  );
40
61
  }
41
62
 
42
- export function ReleaseNotes({ title, entries }: { title: string; entries: VersionNotes[] }) {
63
+ export function ReleaseNotes({ title, entries }: { title?: string; entries: VersionNotes[] }) {
43
64
  return (
44
65
  <article className="mb-8 max-w-prose space-y-5 px-4 not-first:mt-8">
45
- <h2 className="text-2xl">{title}</h2>
46
- {entries.map((entry) => (
47
- <VersionEntry key={entry.version} entry={entry} />
66
+ {title && <h2 className="text-2xl">{title}</h2>}
67
+ {entries.map((entry, i) => (
68
+ // One photo per line: under its newest version's heading.
69
+ <VersionEntry key={entry.version} entry={entry} photo={entry.edition?.line !== entries[i - 1]?.edition?.line} />
48
70
  ))}
49
71
  </article>
50
72
  );
package/src/Screen.tsx CHANGED
@@ -14,7 +14,7 @@
14
14
  // Under the content of every screen is the editor's footer (EditorFooter.tsx), at the bottom of the window when the content
15
15
  // is short (the screen fills the column under the bar, TopBarLayout). Below medium, while the screen has a floating action
16
16
  // button (fabPresence.tsx), there is room for it under the footer (data-clearance): the page is that much taller than the
17
- // window (--editor-fab-clearance, editor.css), so a short screen still has its footer at the bottom of the window and can
17
+ // window (--fab-room and --editor-fab-clearance, editor.css), so a short screen still has its footer at the bottom of the window and can
18
18
  // be scrolled up to show it above the FAB.
19
19
  import { useId, type MouseEvent, type ReactNode } from 'react';
20
20
  import { createPortal } from 'react-dom';
@@ -180,7 +180,7 @@ export default function Screen<T extends string>({
180
180
  <div className="flex flex-1">
181
181
  <main
182
182
  data-clearance={fabs.count > 0}
183
- className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[env(safe-area-inset-bottom)] data-[clearance=true]:pb-[calc(4.5rem+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 medium:pb-4 medium:data-[clearance=true]:pb-4 ${width}`}
183
+ className={`editor-screen flex min-w-0 flex-1 flex-col px-4 pt-6 pb-[env(safe-area-inset-bottom)] data-[clearance=true]:pb-[calc(var(--fab-room)+env(safe-area-inset-bottom))] medium:px-6 medium:pt-8 medium:pb-4 medium:data-[clearance=true]:pb-4 ${width}`}
184
184
  >
185
185
  {/* At expanded and wider: the way here, the title and the status, under the bar. */}
186
186
  <div className="mb-6 hidden min-w-0 items-center gap-x-2 expanded:flex">
@@ -1,11 +1,13 @@
1
- // At the top of the start screen: what was updated since the last visit in this browser, as a card with a close button
2
- // that marks it seen, in the accent (the editor's blue). For the CMS: its new version large, how many new things it brings and the first of them from the
3
- // notes the package carries (bundledNotes.ts: the first with a screenshot large, then the titles and texts of the next),
4
- // and a button to the Morpheme page with all of them; for each app installed from the store: a row with its icon and new
5
- // version, to its Information tab. Exposes UpdatesNotice. The logic is startView.mjs's (changesSince) and
6
- // releaseNotes.mjs's (notesSince), the storage startStorage.ts's, the card, rows and buttons the theme's (themeUi.ts, ui.tsx).
7
- import { useEffect, useState } from 'react';
8
- import { ArrowRight, Sparkle } from '@phosphor-icons/react';
1
+ // At the top of the start screen: what was updated since the last visit in this browser, as a card with Close, which
2
+ // marks it seen, and Read more. For the CMS: the photo of its edition (release-notes/editions/, named after the place the
3
+ // photo is from), 3:2 with rounded corners, above the text in the compact window class and from the medium class up to
4
+ // the left of it, as high as the card; "Morpheme <edition> <version>" (the headline), the place in full (the subhead,
5
+ // with an info button after it that credits the photographer, PhotoCredit), how many new things it brings (the supporting
6
+ // text, bundledNotes.ts) and Read more to the Morpheme page with all of them; for each app installed from the store: a
7
+ // row with its icon and new version, to its Information tab. Exposes UpdatesNotice. The logic is startView.mjs's
8
+ // (changesSince) and releaseNotes.mjs's (notesSince, editionOf), the storage startStorage.ts's, the card, rows, buttons
9
+ // and tooltip the theme's (themeUi.ts, ui.tsx).
10
+ import { useEffect, useRef, useState } from 'react';
9
11
  import { version } from '../package.json';
10
12
  import { locale, t } from './i18n';
11
13
  import { installedApps } from './apps';
@@ -13,17 +15,16 @@ import { apps } from './site';
13
15
  import { changesSince, versioned, type Versions } from './startView.mjs';
14
16
  import { keepStored, SEEN, storedSeen } from './startStorage';
15
17
  import type { RepoFile } from './github';
16
- import { SiteCard } from './themeUi';
17
- import { CloseButton, LinkButton, ListItem, plainListClass } from './ui';
18
+ import { SiteCard, SiteTooltip } from './themeUi';
19
+ import { Svg } from './icon';
20
+ import { Button, LinkButton, ListItem, plainListClass } from './ui';
18
21
  import { CMS_ROUTE } from './CmsPage';
22
+ import { LICENSES_ROUTE } from './Licenses';
19
23
  import { appHref } from './AppList';
20
- import { runningNotes } from './bundledNotes';
21
- import { notesSince, type ReleaseNote } from '../lib/releaseNotes.mjs';
24
+ import { runningEdition, runningNotes } from './bundledNotes';
25
+ import { notesSince } from '../lib/releaseNotes.mjs';
22
26
  import { INFO_KEY } from '../lib/appRoute.mjs';
23
27
 
24
- /** How many of the new things the card shows; the Morpheme page has them all. */
25
- const SHOWN = 3;
26
-
27
28
  /** The CMS's version and those of the apps installed from the store, as the editor is built with them now. */
28
29
  function versionsNow(files: RepoFile[]): Versions {
29
30
  const installed = installedApps(files);
@@ -33,28 +34,61 @@ function versionsNow(files: RepoFile[]): Versions {
33
34
  };
34
35
  }
35
36
 
36
- /** The first new thing with a screenshot, large: the screenshot fills the top (its top part), its title under it. */
37
- function Lead({ note }: { note: Extract<ReleaseNote, { kind: 'card' }> }) {
37
+ /** The info button right after the place's name (the theme's info icon, which every icon pack has), with a rich
38
+ * tooltip: who took the photo, on Unsplash, and a link to its licence under "Licenses". It opens while the mouse is over
39
+ * the button or the tooltip, while focus is in them, and on a tap (a second tap closes it); Escape closes it. Under the
40
+ * button, from its end, at most 15rem wide, so it stays on a phone's screen. */
41
+ function PhotoCredit({ by, line }: { by: string; line: string }) {
42
+ const [open, setOpen] = useState(false);
43
+ const leave = useRef<ReturnType<typeof setTimeout>>(undefined);
44
+ // When it opened: a tap focuses the button (which opens it) just before its click, which must not close it again.
45
+ const openedAt = useRef(0);
46
+ const show = () => {
47
+ clearTimeout(leave.current);
48
+ if (!open) openedAt.current = Date.now();
49
+ setOpen(true);
50
+ };
51
+ const hide = () => {
52
+ clearTimeout(leave.current);
53
+ leave.current = setTimeout(() => setOpen(false), 150);
54
+ };
55
+ // Escape closes it wherever the pointer and the focus are (WCAG 1.4.13).
56
+ useEffect(() => {
57
+ if (!open) return;
58
+ const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false);
59
+ document.addEventListener('keydown', onKey);
60
+ return () => document.removeEventListener('keydown', onKey);
61
+ }, [open]);
62
+ useEffect(() => () => clearTimeout(leave.current), []);
38
63
  return (
39
- <div className="overflow-hidden rounded-xl bg-accent-container text-on-accent-container">
40
- <img src={note.image} alt="" className="block h-44 w-full object-cover object-top" />
41
- <p className="px-4 py-3 font-medium">{note.title}</p>
64
+ <div
65
+ className="-my-3 -ms-2.5 inline-flex align-middle"
66
+ onMouseEnter={show}
67
+ onMouseLeave={hide}
68
+ onFocus={show}
69
+ onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && hide()}
70
+ >
71
+ <SiteTooltip
72
+ open={open}
73
+ // Only `open` shows it: not the theme's fallback on hover and focus, which Escape could not close.
74
+ className="[&:not([data-tooltip-open])>.tooltip]:invisible [&:not([data-tooltip-open])>.tooltip]:opacity-0 [&>.tooltip]:start-auto [&>.tooltip]:end-0 [&>.tooltip]:top-[calc(100%-0.5rem)] [&>.tooltip]:bottom-auto [&>.tooltip]:max-w-60"
75
+ supportingText={t.library.photoBy(by)}
76
+ action={{ label: t.library.photoLicense, href: `#${LICENSES_ROUTE}/${encodeURIComponent(`editions/${line}`)}` }}
77
+ >
78
+ <button
79
+ type="button"
80
+ aria-label={t.library.photoInfo}
81
+ aria-expanded={open}
82
+ onClick={() => (open && Date.now() - openedAt.current < 400 ? undefined : open ? setOpen(false) : show())}
83
+ className="grid size-12 place-items-center rounded-full text-muted focus-visible:outline-2 focus-visible:outline-offset-[-8px]"
84
+ >
85
+ <Svg name="info" size={20} />
86
+ </button>
87
+ </SiteTooltip>
42
88
  </div>
43
89
  );
44
90
  }
45
91
 
46
- /** One more new thing: a sparkle and its title (a card) or its text (a line). */
47
- function Highlight({ note }: { note: ReleaseNote }) {
48
- return (
49
- <li className="flex items-center gap-3 rounded-xl bg-accent-container p-3 text-on-accent-container">
50
- <span className="grid size-10 shrink-0 place-items-center rounded-full bg-accent text-on-accent">
51
- <Sparkle size={20} weight="fill" aria-hidden />
52
- </span>
53
- <span className={note.kind === 'card' ? 'font-medium' : 'text-sm'}>{note.kind === 'card' ? note.title : note.text}</span>
54
- </li>
55
- );
56
- }
57
-
58
92
  /** The first visit, and a visit with nothing new, only notes the versions. */
59
93
  export function UpdatesNotice({ files }: { files: RepoFile[] }) {
60
94
  const now = versionsNow(files);
@@ -74,48 +108,49 @@ export function UpdatesNotice({ files }: { files: RepoFile[] }) {
74
108
  return app ? [{ app, version: c.version }] : [];
75
109
  });
76
110
  const news = cms ? notesSince(runningNotes, cms.from, locale) : [];
77
- const lead = news.find((note): note is Extract<ReleaseNote, { kind: 'card' }> => note.kind === 'card' && !!note.image);
78
- const rest = news.filter((note) => note !== lead);
111
+ const edition = cms && runningEdition;
112
+ const place = edition && (edition.place[locale] ?? edition.place['en-US']);
113
+ const title = cms
114
+ ? edition
115
+ ? t.library.cmsEdition(edition.name, cms.version)
116
+ : t.library.cmsVersion(cms.version)
117
+ : t.library.appsUpdated(updatedApps.length);
79
118
  return (
80
- <SiteCard
81
- kind="filled"
82
- as="section"
83
- role="status"
84
- aria-labelledby="updates-title"
85
- className="relative mb-8 text-on-accent [--card-filled-surface:var(--accent)] [--card-padding:0] [--icon-button-standard-text:var(--on-accent)]"
86
- >
87
- <CloseButton onClick={close} />
88
- <div className="flex items-center gap-4 p-4 pe-14">
89
- <span className="grid size-12 shrink-0 place-items-center rounded-full bg-on-accent text-accent">
90
- <Sparkle size={26} weight="fill" aria-hidden />
91
- </span>
92
- <div className="min-w-0">
93
- <h2 id="updates-title" className="text-sm font-medium">
94
- {t.library.updatesTitle}
119
+ <SiteCard kind="outlined" as="section" role="status" aria-labelledby="updates-title" className="mb-8 [--card-padding:0]">
120
+ <div className="grid medium:grid-cols-[auto_1fr]">
121
+ {edition && (
122
+ <div className="relative aspect-[3/2] w-full medium:h-full medium:w-auto">
123
+ <img src={edition.image} alt="" className="absolute inset-0 size-full rounded-xl object-cover" />
124
+ </div>
125
+ )}
126
+ <div className="flex min-w-0 flex-col p-4">
127
+ <h2
128
+ id="updates-title"
129
+ className="text-[length:var(--type-headline-small-size)] leading-[var(--type-headline-small-line-height)] [overflow-wrap:anywhere]"
130
+ >
131
+ {title}
95
132
  </h2>
96
- <p className="text-2xl leading-tight">{cms ? t.library.cmsVersion(cms.version) : t.library.appsUpdated(updatedApps.length)}</p>
97
- {news.length > 0 && <p className="text-sm opacity-85">{t.library.newThings(news.length)}</p>}
133
+ {place && (
134
+ <p className="mt-1 text-sm">
135
+ {place}
136
+ {edition.by && <PhotoCredit by={edition.by} line={edition.line} />}
137
+ </p>
138
+ )}
139
+ {news.length > 0 && <p className="mt-2 text-sm text-muted">{t.library.newThings(news.length)}</p>}
140
+ <div className="mt-auto flex flex-wrap justify-end gap-2 pt-4">
141
+ <Button kind="text" size="small" onClick={close}>
142
+ {t.common.close}
143
+ </Button>
144
+ {cms && (
145
+ <LinkButton kind="filled" size="small" href={`#${CMS_ROUTE}`}>
146
+ {t.library.readMore}
147
+ </LinkButton>
148
+ )}
149
+ </div>
98
150
  </div>
99
151
  </div>
100
- {news.length > 0 && (
101
- <div className="space-y-2 px-4">
102
- {lead && <Lead note={lead} />}
103
- <ul className="space-y-2">
104
- {rest.slice(0, lead ? SHOWN - 1 : SHOWN).map((note) => (
105
- <Highlight key={note.id} note={note} />
106
- ))}
107
- </ul>
108
- </div>
109
- )}
110
- {cms && (
111
- <div className="px-4 pt-4 pb-4">
112
- <LinkButton kind="tonal" href={`#${CMS_ROUTE}`} icon={ArrowRight}>
113
- {t.library.seeWhatsNew}
114
- </LinkButton>
115
- </div>
116
- )}
117
152
  {updatedApps.length > 0 && (
118
- <ul className={`${plainListClass} mx-4 mb-4 overflow-hidden rounded-xl bg-accent-container text-on-accent-container`}>
153
+ <ul className={`${plainListClass} border-t border-line`}>
119
154
  {updatedApps.map(({ app, version }) => (
120
155
  <ListItem
121
156
  key={app.id}
@@ -0,0 +1,42 @@
1
+ // A minor line's own page (#morpheme/<line>, e.g. #morpheme/0.13), from "Earlier versions" on the Morpheme page:
2
+ // "Morpheme 0.13" with the notes of each of its versions (ReleaseNotes), newest first. Exposes VersionLinePage and
3
+ // EarlierVersions (the rows to these pages). Depends on bundledNotes.ts (runningNotes, runningLine) and
4
+ // lib/releaseNotes.mjs (versionLines).
5
+ import Screen from './Screen';
6
+ import { CMS_ROUTE } from './CmsPage';
7
+ import { ReleaseNotes } from './ReleaseNotes';
8
+ import { EmptyNote, Group, ListItem } from './ui';
9
+ import { runningLine, runningNotes } from './bundledNotes';
10
+ import { versionLines } from '../lib/releaseNotes.mjs';
11
+ import { t } from './i18n';
12
+
13
+ /** The lines older than the running one, newest first. */
14
+ const earlier = () => versionLines(runningNotes).filter(({ line }) => line !== runningLine);
15
+
16
+ /** "Earlier versions": a row per older minor line, with how many versions it has, to its page. */
17
+ export function EarlierVersions() {
18
+ const lines = earlier();
19
+ if (!lines.length) return null;
20
+ return (
21
+ <Group title={t.cms.earlierVersions}>
22
+ {lines.map(({ line, entries }) => (
23
+ <ListItem
24
+ key={line}
25
+ href={`#${CMS_ROUTE}/${line}`}
26
+ headline={t.library.cmsVersion(line)}
27
+ supporting={t.cms.versionCount(entries.length)}
28
+ arrow
29
+ />
30
+ ))}
31
+ </Group>
32
+ );
33
+ }
34
+
35
+ export function VersionLinePage({ line }: { line: string }) {
36
+ const found = earlier().find((x) => x.line === line);
37
+ return (
38
+ <Screen title={t.library.cmsVersion(line)} back={{ href: `#${CMS_ROUTE}`, label: t.common.parts.morpheme }}>
39
+ {found ? <ReleaseNotes entries={found.entries} /> : <EmptyNote>{t.cms.lineNotFound}</EmptyNote>}
40
+ </Screen>
41
+ );
42
+ }
@@ -1,11 +1,46 @@
1
- // The release notes of the minor line this package is in (x.y.0 up to its version): read at build time from the package's own release-notes/ files.
2
- // Exposes runningNotes (the entries the Morpheme page shows, newest first; empty when the line has none).
1
+ // The release notes of every version up to this package's: read at build time from its own release-notes/ files and CHANGELOG.md.
2
+ // Exposes runningNotes (the entries the Morpheme page shows, newest first, each with its date and its edition: every
3
+ // version with notes, then the older ones from CHANGELOG.md; on a test version first the notes merged since the last
4
+ // release) and runningEdition
5
+ // (the edition this version belongs to: the place its photo is from, short and in full, and the photo; undefined when there is none)
6
+ // and editionPhotos (each edition's photo by its minor line).
3
7
  // Depends on lib/releaseNotes.mjs (bundledNotes) and package.json's version. The globs are relative, so they work here and in a site's node_modules.
4
- import { bundledNotes } from '../lib/releaseNotes.mjs';
8
+ import { bundledNotes, editionOf, minorOf } from '../lib/releaseNotes.mjs';
5
9
  import { version } from '../package.json';
6
- import type { ReleasedNotes } from '../lib/releaseNotes.mjs';
10
+ import changelog from '../CHANGELOG.md?raw';
11
+ import type { ReleasedNotes, ReleaseNote } from '../lib/releaseNotes.mjs';
7
12
 
8
13
  const files = import.meta.glob<ReleasedNotes>('../release-notes/i18n/*/*.json', { eager: true, import: 'default' });
14
+ // The notes merged since the last release: a test version (npm tag next) shows them as its own.
15
+ const pending = import.meta.glob<ReleaseNote>('../release-notes/i18n/*/pending/*.json', { eager: true, import: 'default' });
9
16
  const images = import.meta.glob<string>('../release-notes/i18n/*/*/*.webp', { eager: true, query: '?url', import: 'default' });
10
17
 
11
- export const runningNotes = bundledNotes(files, images, version);
18
+ const editions = import.meta.glob<{ name?: string; place?: Record<string, string>; photo?: { by?: string } }>(
19
+ '../release-notes/editions/*.json',
20
+ {
21
+ eager: true,
22
+ import: 'default',
23
+ },
24
+ );
25
+ const editionImages = import.meta.glob<string>('../release-notes/editions/*.webp', { eager: true, query: '?url', import: 'default' });
26
+
27
+ export const runningEdition = editionOf(editions, editionImages, version);
28
+
29
+ export const runningNotes = bundledNotes(files, images, version, pending, changelog).map((entry) => ({
30
+ ...entry,
31
+ edition: editionOf(editions, editionImages, entry.version),
32
+ }));
33
+
34
+ /** Each edition's photo, whole, by its version: on its page under "Licenses" (LicensePage). */
35
+ export const editionPhotos: Record<string, string> = Object.fromEntries(
36
+ Object.entries(editionImages).map(([path, url]) => [
37
+ path
38
+ .split('/')
39
+ .pop()!
40
+ .replace(/\.webp$/, ''),
41
+ url,
42
+ ]),
43
+ );
44
+
45
+ /** The minor line this package is in ("0.14"): its versions show on the Morpheme page, the older lines on pages of their own. */
46
+ export const runningLine = minorOf(version);
package/src/editor.css CHANGED
@@ -8,6 +8,9 @@
8
8
  --editor-menu-surface: var(--surface-container-lowest);
9
9
  --editor-menu-shadow: var(--elevation-2);
10
10
 
11
+ /* The room a screen leaves for its FAB under the footer: Screen.tsx's padding and --editor-fab-clearance (below medium). */
12
+ --fab-room: 4.5rem;
13
+
11
14
  /* How a check went (CheckList: the SEO rating), as PageSpeed shows it: the mark before a check that is fine and one
12
15
  that can be done better, and the score's ring (from 90 good, from 50 average, else bad) with its number and the
13
16
  light circle in it; the same in every theme. */
@@ -424,7 +427,7 @@
424
427
  the window and can be scrolled up to show it above the FAB. */
425
428
  @media (width < theme(--breakpoint-medium)) {
426
429
  :root:has(.editor-screen[data-clearance='true']) {
427
- --editor-fab-clearance: 4.5rem;
430
+ --editor-fab-clearance: var(--fab-room);
428
431
  }
429
432
  }
430
433
 
@@ -8,6 +8,10 @@ export const cms: Messages['cms'] = {
8
8
  available: (v) => `Version ${v} er klar`,
9
9
  whatsNew: 'Nyheder',
10
10
  newInYourVersion: 'Nyt i din version',
11
+ newIn: (name) => `Nyt i ${name}`,
12
+ earlierVersions: 'Tidligere versioner',
13
+ versionCount: (count) => (count === 1 ? '1 version' : `${count} versioner`),
14
+ lineNotFound: 'Versionen findes ikke',
11
15
  alsoInThisVersion: 'Også i denne version',
12
16
  storeConnection: 'Forbindelse til Appstore',
13
17
  major: 'En ny hovedversion installeres ikke af sig selv.',
@@ -8,21 +8,24 @@ export const library: Messages['library'] = {
8
8
  continueTitle: 'Fortsæt, hvor du slap',
9
9
  continueEmpty: 'De sider og indlæg, du redigerer, vises her.',
10
10
  recentEdit: (kind, when) => `${kind} · Redigeret ${when}`,
11
- updatesTitle: 'Opdateret siden dit sidste besøg',
11
+ cmsEdition: (edition, version) => `Morpheme ${edition} ${version}`,
12
12
  appsUpdated: (count) => (count === 1 ? '1 app er opdateret' : `${count} apps er opdateret`),
13
13
  newThings: (count) => (count === 1 ? '1 nyhed' : `${count} nyheder`),
14
- seeWhatsNew: 'Se, hvad der er nyt',
14
+ readMore: 'Læs mere',
15
+ photoInfo: 'Om billedet',
16
+ photoBy: (name) => `Foto af ${name} på Unsplash`,
17
+ photoLicense: 'Se licens',
15
18
  appUpdatedTo: (version) => `Opdateret til ${version}`,
16
19
  githubAccount: (login) => `GitHub-konto: ${login}`,
17
20
  logOut: 'Log ud',
18
21
  notifications: 'Notifikationer',
19
22
  notificationsNew: (count) => `Notifikationer, ${count} nye`,
20
23
  noNotifications: 'Ingen notifikationer',
21
- clearNotifications: 'Ryd notifikationer',
24
+ clearNotifications: 'Ryd',
22
25
  deleteSelected: (count) => `Slet ${count} valgte`,
23
26
  cancelSelection: 'Annuller',
24
27
  openWebsite: 'Åbn hjemmesiden',
25
- notificationChoices: 'Vælg notifikationer',
28
+ notificationChoices: 'Indstillinger',
26
29
  onDevice: 'På telefonen',
27
30
  onDeviceHint: 'Også når editoren er lukket.',
28
31
  deviceBlocked: 'Telefonen eller browseren tillader ikke notifikationer fra editoren. Slå dem til i dens indstillinger.',
@@ -10,6 +10,13 @@ export const cms = {
10
10
  available: (v: string) => `Version ${v} is available`,
11
11
  whatsNew: 'What’s new',
12
12
  newInYourVersion: 'New in your version',
13
+ /** Over a version's notes on that page: "New in Morpheme Braies 0.14.3" (library.cmsEdition, else cmsVersion). */
14
+ newIn: (name: string) => `New in ${name}`,
15
+ /** The older minor lines under the running one's notes, each a row to its page (VersionLinePage.tsx). */
16
+ earlierVersions: 'Earlier versions',
17
+ versionCount: (count: number) => (count === 1 ? '1 version' : `${count} versions`),
18
+ /** A minor line's page (#morpheme/<line>) for a line the package has no notes for. */
19
+ lineNotFound: 'This version doesn’t exist',
13
20
  alsoInThisVersion: 'Also in this version',
14
21
  storeConnection: 'Store connection',
15
22
  major: 'A new major version isn’t installed by itself.',
@@ -15,13 +15,19 @@ export const library = {
15
15
  /** Under each of them: its kind ("Page") and when it was saved ("today at 1:42 PM", when.ts). */
16
16
  recentEdit: (kind: string, when: string) => `${kind} · Edited ${when}`,
17
17
  /** At the top of the start screen (UpdatesNotice.tsx): the card with what was updated since the last visit in this
18
- * browser. Its title, over the CMS's new version (cmsVersion) or, with only apps updated, how many. */
19
- updatesTitle: 'Updated since your last visit',
18
+ * browser. Its headline: the CMS's name, its edition (the place the edition's photo is from: "Braies") and its new
19
+ * version; without an edition, cmsVersion; with only apps updated, how many (appsUpdated). */
20
+ cmsEdition: (edition: string, version: string) => `Morpheme ${edition} ${version}`,
20
21
  appsUpdated: (count: number) => (count === 1 ? '1 app updated' : `${count} apps updated`),
21
22
  /** Under the CMS's new version: how many new things its release notes have since the version seen last. */
22
23
  newThings: (count: number) => (count === 1 ? '1 new thing' : `${count} new things`),
23
24
  /** The button to the Morpheme page, with all the release notes. */
24
- seeWhatsNew: 'See what’s new',
25
+ readMore: 'Read more',
26
+ /** The info button on the edition's photo in that card, and its tooltip: who took the photo, and a link to its
27
+ * licence under "Licenses". */
28
+ photoInfo: 'About the photo',
29
+ photoBy: (name: string) => `Photo by ${name} on Unsplash`,
30
+ photoLicense: 'See license',
25
31
  /** Under an updated app's name. */
26
32
  appUpdatedTo: (version: string) => `Updated to ${version}`,
27
33
  githubAccount: (login: string) => `GitHub account: ${login}`,
@@ -31,7 +37,8 @@ export const library = {
31
37
  /** The bell's name while some are not seen yet. */
32
38
  notificationsNew: (count: number) => `Notifications, ${count} new`,
33
39
  noNotifications: 'No notifications',
34
- clearNotifications: 'Clear notifications',
40
+ /** The buttons at the bottom of the side sheet: one clears all, one opens "Konto" to choose the types. */
41
+ clearNotifications: 'Clear',
35
42
  /** While some are selected (a long press on one, Notifications.tsx): the button that deletes them, and the one that
36
43
  * unselects them. */
37
44
  deleteSelected: (count: number) => `Delete ${count} selected`,
@@ -40,7 +47,7 @@ export const library = {
40
47
  openWebsite: 'Open website',
41
48
  /** On "Konto" › "Notifikationer" (Library.tsx NotificationChoices, notificationData.ts). */
42
49
  /** The sliders on "Notifikationer": to "Konto", where they are turned on or off. */
43
- notificationChoices: 'Choose notifications',
50
+ notificationChoices: 'Settings',
44
51
  onDevice: 'On the phone',
45
52
  onDeviceHint: 'Also when the editor is closed.',
46
53
  deviceBlocked: 'The phone or browser doesn’t allow notifications from the editor. Turn them on in its settings.',