@morphemecms/cms 0.5.0-next-20261104200208 → 0.5.0

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,11 +1,18 @@
1
1
  # @morphemecms/cms
2
2
 
3
- ## 0.5.0-next-20261104200208
3
+ ## 0.5.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
7
  - 322a404: Connect a site to the Morpheme store from the editor's Appstore. A key set by hand in MORPHEME_STORE_KEY still works.
8
8
 
9
+ ### Patch Changes
10
+
11
+ - eb1f36f: A site's build reads Phosphor's icons one at a time, so it passes on Cloudflare's build image, which allows 1024 open files.
12
+ - 6844676: The editor's start screen shows the installed CMS version before the update time, e.g. "Morpheme 0.5.0 · Updated 19.26". A pre-release shows short ("0.5.0-next"); a click shows the full version.
13
+ - 2fa2bb3: New sites install a fixed postcss-selector-parser (6.1.5) under @tailwindcss/typography, which closes CVE-2026-104844.
14
+ - f343523: The package has the types of the theme's `editor/looks.mjs`, so a site's type check (`tsc`) passes again.
15
+
9
16
  ## 0.4.0
10
17
 
11
18
  ### Minor Changes
package/lib/licenses.mjs CHANGED
@@ -428,7 +428,7 @@ const HELPERS = [
428
428
  [/^\0vite\//, 'vite'],
429
429
  [/^\0rolldown\//, 'rolldown'],
430
430
  ];
431
- const OWN_VIRTUAL = [/^\0virtual:app-folders$/, /^\0site-theme-icons?(:|$)/];
431
+ const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons)$/, /^\0site-theme-icons?(:|$)/];
432
432
 
433
433
  /** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
434
434
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.5.0-next-20261104200208",
3
+ "version": "0.5.0",
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",
@@ -138,5 +138,10 @@
138
138
  "vite": "^8.3.4",
139
139
  "wrangler": "^4.152.0",
140
140
  "yaml": "^2.9.1"
141
+ },
142
+ "overrides": {
143
+ "@tailwindcss/typography": {
144
+ "postcss-selector-parser": "^6.1.5"
145
+ }
141
146
  }
142
147
  }
@@ -0,0 +1,93 @@
1
+ // Tests that every .mjs module imported by TypeScript or Astro files in the package is included in tsconfig.types.json.
2
+ // This is required because TypeScript reads no types from JavaScript in node_modules, so a site's tsc fails without a .d.mts.
3
+
4
+ import assert from 'node:assert/strict';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import test from 'node:test';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const PACKAGE = fileURLToPath(new URL('..', import.meta.url));
11
+
12
+ const tsconfigRaw = fs.readFileSync(path.join(PACKAGE, 'tsconfig.types.json'), 'utf-8');
13
+ const tsconfigJson = JSON.parse(
14
+ tsconfigRaw
15
+ .split('\n')
16
+ .filter((line) => !line.trimStart().startsWith('//'))
17
+ .join('\n'),
18
+ );
19
+ const include = tsconfigJson.include;
20
+
21
+ function matches(filePath, pattern) {
22
+ if (pattern.includes('**')) {
23
+ const dir = pattern.replace('/**/*.mjs', '');
24
+ return filePath.startsWith(`${dir}/`) && filePath.endsWith('.mjs');
25
+ }
26
+ return filePath === pattern;
27
+ }
28
+
29
+ function sources(dir) {
30
+ const results = [];
31
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
34
+ continue;
35
+ }
36
+ const fullPath = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ results.push(...sources(fullPath));
39
+ } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx') || entry.name.endsWith('.astro')) {
40
+ if (!entry.name.endsWith('.d.ts')) {
41
+ results.push(path.relative(PACKAGE, fullPath).replace(/\\/g, '/'));
42
+ }
43
+ }
44
+ }
45
+ return results;
46
+ }
47
+
48
+ function mjsImports(text) {
49
+ const regex = /(?:from\s+|import\s*\()\s*(['"])([^'"]+\.mjs)\1/g;
50
+ const specs = [];
51
+ let match;
52
+ while ((match = regex.exec(text)) !== null) {
53
+ specs.push(match[2]);
54
+ }
55
+ return specs;
56
+ }
57
+
58
+ function resolve(file, spec) {
59
+ if (spec.startsWith('@theme/')) {
60
+ return `theme/${spec.slice('@theme/'.length)}`;
61
+ }
62
+ if (spec.startsWith('.')) {
63
+ const dir = path.dirname(file);
64
+ return path.relative(PACKAGE, path.join(dir, spec)).replace(/\\/g, '/');
65
+ }
66
+ return null;
67
+ }
68
+
69
+ test('each .mjs a .ts, .tsx or .astro file imports is in tsconfig.types.json include', () => {
70
+ const missing = [];
71
+ const files = sources(PACKAGE);
72
+ for (const file of files) {
73
+ const content = fs.readFileSync(path.join(PACKAGE, file), 'utf-8');
74
+ const imports = mjsImports(content);
75
+ for (const spec of imports) {
76
+ const resolvedPath = resolve(file, spec);
77
+ if (resolvedPath !== null) {
78
+ const isMatched = include.some((pattern) => matches(resolvedPath, pattern));
79
+ if (!isMatched) {
80
+ missing.push({ file, spec, path: resolvedPath });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ assert.deepEqual(missing, []);
86
+ });
87
+
88
+ test('matches', () => {
89
+ assert.equal(matches('theme/editor/looks.mjs', 'theme/**/*.mjs'), true);
90
+ assert.equal(matches('theme/editor/looks.mjs', 'lib/**/*.mjs'), false);
91
+ assert.equal(matches('scripts/releases.mjs', 'scripts/releases.mjs'), true);
92
+ assert.equal(matches('scripts/other.mjs', 'scripts/releases.mjs'), false);
93
+ });
package/site/theme.d.mts CHANGED
@@ -30,6 +30,10 @@ export function motionProblems(): string[];
30
30
  /** What is wrong with the system's icon packs (packProblems; a theme's own are its themeProblems) and the choice of one:
31
31
  * src/data/icons.json naming one that is not there. */
32
32
  export function iconPackProblems(): string[];
33
+ /** The module code of all Phosphor's icons in `folder` (its `regular/` folder), by the keys `import.meta.glob` gave, the
34
+ * theme's (`icons`, as themeIcons returns them) in place of Phosphor's own. The files are read one at a time: a glob
35
+ * opens them all at once, and the build image's limit of open files (1024) stops the build. */
36
+ export function allIconsModule(folder: any, icons: any): string;
33
37
  /** What a theme is missing (the standard theme: of THEME_FILES and the look of every standard block, another: of
34
38
  * OWN_FILES), what is wrong in its theme.json (name, version, author, icon), a block of its own (a folder in its
35
39
  * blocks/ that is not the look of a standard block, or with a block's contract in it), a part nothing uses (in a
@@ -76,6 +80,8 @@ export const THEME_FILES: string[];
76
80
  export function installedThemes(): string[];
77
81
  /** What every theme has itself. */
78
82
  export const OWN_FILES: string[];
83
+ /** The virtual module with all Phosphor's icons (regular) as text (src/allIcons.ts). */
84
+ export const ALL_ICONS: "virtual:all-icons";
79
85
  export function aliases(): {
80
86
  '@data': string;
81
87
  '@saturn': string;
package/site/theme.mjs CHANGED
@@ -4,9 +4,9 @@
4
4
  // theme or the look of a block, is the standard theme's, styled by the theme in use's CSS. The site and the editor
5
5
  // reach the theme as @theme (themePlugin: the theme in use's file, else the standard theme's), and the site's own
6
6
  // parts (src/site/) as @site (aliases): for packages/editor/astro.mjs and packages/editor/scripts/check-content.mjs. Stops with what is
7
- // wrong, when the theme named is not installed.
7
+ // wrong, when the theme named is not installed. Also the virtual module with all Phosphor's icons (allIconsModule).
8
8
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
- import { relative } from 'node:path';
9
+ import { join, relative } from 'node:path';
10
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
11
  import { svgParts } from '@morphemecms/cms/lib/svg.mjs';
12
12
  import { declarationProblems, ownerId } from '@morphemecms/cms/lib/licenses.mjs';
@@ -268,13 +268,35 @@ function reactIcons(icons) {
268
268
  return lines.join('\n');
269
269
  }
270
270
 
271
- /** An icon's SVG file as Phosphor's are (in the text's colour, `fill="currentColor"`), for an import of Phosphor's. */
272
- const fileIcon = (svg) => {
271
+ /** The text of an icon's SVG file as Phosphor's are (in the text's colour, `fill="currentColor"`), not JSON-encoded. */
272
+ const themedSvg = (svg) => {
273
273
  const { viewBox, inner } = svgParts(svg);
274
274
  const box = viewBox ? ` viewBox="${viewBox}"` : '';
275
- return `export default ${JSON.stringify(`<svg xmlns="http://www.w3.org/2000/svg"${box} fill="currentColor">${inner}</svg>`)};`;
275
+ return `<svg xmlns="http://www.w3.org/2000/svg"${box} fill="currentColor">${inner}</svg>`;
276
276
  };
277
277
 
278
+ /** An icon's SVG file as Phosphor's are, for an import of Phosphor's. */
279
+ const fileIcon = (svg) => `export default ${JSON.stringify(themedSvg(svg))};`;
280
+
281
+ /** The virtual module with all Phosphor's icons (regular) as text (src/allIcons.ts). */
282
+ export const ALL_ICONS = 'virtual:all-icons';
283
+
284
+ /** The module code of all Phosphor's icons in `folder` (its `regular/` folder), by the keys `import.meta.glob` gave, the
285
+ * theme's (`icons`, as themeIcons returns them) in place of Phosphor's own. The files are read one at a time: a glob
286
+ * opens them all at once, and the build image's limit of open files (1024) stops the build. */
287
+ export function allIconsModule(folder, icons) {
288
+ const all = {};
289
+ for (const file of readdirSync(folder)
290
+ .filter((f) => f.endsWith('.svg'))
291
+ .sort()) {
292
+ const name = file.slice(0, -'.svg'.length);
293
+ all[`/node_modules/@phosphor-icons/core/assets/regular/${file}`] = svgParts(icons[name])
294
+ ? themedSvg(icons[name])
295
+ : readFileSync(join(folder, file), 'utf8');
296
+ }
297
+ return `export const allIcons = ${JSON.stringify(all)};`;
298
+ }
299
+
278
300
  /** What a theme is missing (the standard theme: of THEME_FILES and the look of every standard block, another: of
279
301
  * OWN_FILES), what is wrong in its theme.json (name, version, author, icon), a block of its own (a folder in its
280
302
  * blocks/ that is not the look of a standard block, or with a block's contract in it), a part nothing uses (in a
@@ -444,6 +466,7 @@ export function themePlugin() {
444
466
  resolveId(source, importer) {
445
467
  // The theme's icons import Phosphor's own.
446
468
  if (source === PHOSPHOR_REACT) return own && importer !== REACT_ICONS ? REACT_ICONS : null;
469
+ if (source === ALL_ICONS) return '\0' + ALL_ICONS;
447
470
  const icon = source.match(PHOSPHOR_FILE)?.[1];
448
471
  if (icon) return svgParts(icons[icon]) ? FILE_ICON + icon : null;
449
472
  if (source.startsWith('@site/')) return siteFile(source.slice('@site/'.length));
@@ -458,6 +481,8 @@ export function themePlugin() {
458
481
  },
459
482
  load(id) {
460
483
  if (id === REACT_ICONS) return reactIcons(icons);
484
+ if (id === '\0' + ALL_ICONS)
485
+ return allIconsModule(fileURLToPath(new URL('node_modules/@phosphor-icons/core/assets/regular/', root)), icons);
461
486
  if (id.startsWith(FILE_ICON)) return fileIcon(icons[id.slice(FILE_ICON.length)]);
462
487
  return null;
463
488
  },
@@ -0,0 +1,54 @@
1
+ // Tests of allIconsModule (site/theme.mjs): all Phosphor's icons as one module, read one file at a time.
2
+ // Run with `node --test site/theme.test.mjs`.
3
+ import { strict as assert } from 'node:assert';
4
+ import { mkdtempSync, writeFileSync } from 'node:fs';
5
+ import { registerHooks } from 'node:module';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { test } from 'node:test';
9
+
10
+ // theme.mjs imports this package by its name, which only works from an installed copy: point the name at this folder.
11
+ const PACKAGE = '@morphemecms/cms/';
12
+ const pkg = new URL('../', import.meta.url);
13
+ registerHooks({
14
+ resolve: (specifier, context, next) =>
15
+ specifier.startsWith(PACKAGE) ? next(new URL(specifier.slice(PACKAGE.length), pkg).href, context) : next(specifier, context),
16
+ });
17
+ const { allIconsModule } = await import('./theme.mjs');
18
+
19
+ const PREFIX = '/node_modules/@phosphor-icons/core/assets/regular/';
20
+ const A = '<svg viewBox="0 0 1 1"><path d="A"/></svg>';
21
+ const B = '<svg viewBox="0 0 2 2"><path d="B"/></svg>';
22
+
23
+ const folder = mkdtempSync(join(tmpdir(), 'icons-'));
24
+ writeFileSync(join(folder, 'a.svg'), A);
25
+ writeFileSync(join(folder, 'b.svg'), B);
26
+ writeFileSync(join(folder, 'c.txt'), 'not an icon');
27
+
28
+ const parse = (code) => JSON.parse(code.slice('export const allIcons = '.length, -1));
29
+
30
+ test('the keys are the files, as import.meta.glob gave them', () => {
31
+ assert.deepEqual(Object.keys(parse(allIconsModule(folder, {}))), [`${PREFIX}a.svg`, `${PREFIX}b.svg`]);
32
+ });
33
+
34
+ test("Phosphor's icons are the file text, unchanged", () => {
35
+ assert.deepEqual(parse(allIconsModule(folder, {})), { [`${PREFIX}a.svg`]: A, [`${PREFIX}b.svg`]: B });
36
+ });
37
+
38
+ test("a theme's icon is in Phosphor's place", () => {
39
+ const themed = '<svg viewBox="0 0 10 10"><path d="M0 0"/></svg>';
40
+ const all = parse(allIconsModule(folder, { a: themed }));
41
+ assert.equal(
42
+ all[`${PREFIX}a.svg`],
43
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" fill="currentColor"><path d="M0 0"/></svg>',
44
+ );
45
+ assert.equal(all[`${PREFIX}b.svg`], B);
46
+ });
47
+
48
+ test('a file that is not an svg has no key', () => {
49
+ assert.ok(!Object.keys(parse(allIconsModule(folder, {}))).some((k) => k.endsWith('c.txt')));
50
+ });
51
+
52
+ test('the code is a module that exports allIcons', () => {
53
+ assert.ok(allIconsModule(folder, {}).startsWith('export const allIcons = '));
54
+ });
@@ -0,0 +1,14 @@
1
+ // The CMS's version as plain text on the start screen's status line; a pre-release's short form swaps to the full version on a click.
2
+ // Exposes CmsVersion. Depends on cmsVersionView.mjs (shortVersion), SwapText and package.json's version.
3
+ import { version } from '../package.json';
4
+ import { shortVersion } from './cmsVersionView.mjs';
5
+ import { SwapText } from './SwapText';
6
+ import { t } from './i18n';
7
+
8
+ // A release shows its version as it is: plain text. A pre-release shows it short; clicking swaps it to the full version, with
9
+ // the build stamp, in place. `title` shows the full version on mouse hover. No tooltip: the phone's status line clips overflow.
10
+ export function CmsVersion() {
11
+ const short = shortVersion(version) as string;
12
+ if (short === version) return <>{t.library.cmsVersion(version)}</>;
13
+ return <SwapText text={t.library.cmsVersion(short)} swapped={t.library.cmsVersion(version)} title={version} />;
14
+ }
package/src/Library.tsx CHANGED
@@ -33,6 +33,7 @@ import { byText, entryData, type Collection, type Part, type PartScreen } from '
33
33
  import { config, countsOf, header, relationsOf, siteName } from '@site/editor';
34
34
  import { iconPack, iconPacks } from './iconPacks';
35
35
  import { theme, themes, type ThemeInfo } from './themes';
36
+ import { StartStatus } from './StartStatus';
36
37
  import {
37
38
  appearance,
38
39
  backFrom,
@@ -155,14 +156,14 @@ function PartRow({ id, label, href, count }: { id: string } & ReturnType<typeof
155
156
  return <ListItem href={href} icon={icons[id]} headline={label} count={count} arrow />;
156
157
  }
157
158
 
158
- // The start screen, with when the site was last updated (built on Cloudflare, App.tsx) under the title;
159
+ // The start screen, with the CMS version and when the site was last updated (StartStatus.tsx) under the title;
159
160
  // on a computer the groups two side by side.
160
161
  export function Start({ files, builtAt }: { files: RepoFile[]; builtAt: Date | null }) {
161
162
  return (
162
163
  <Screen
163
164
  title={siteName}
164
165
  wide
165
- status={builtAt ? t.library.siteUpdated(when(builtAt, { short: true })) : undefined}
166
+ status={<StartStatus builtAt={builtAt} />}
166
167
  // The notifications, the settings and the account in the top bar, on a phone too (Screen.tsx).
167
168
  home
168
169
  >
@@ -0,0 +1,24 @@
1
+ // The start screen's status line: from expanded up the CMS version, a separator and the update time; below it (a phone) only the
2
+ // update time, which a tap swaps for the short version. Exposes StartStatus. Depends on CmsVersion, SwapText, when and i18n.
3
+ import { CmsVersion } from './CmsVersion';
4
+ import { SwapText } from './SwapText';
5
+ import { shortVersion } from './cmsVersionView.mjs';
6
+ import { version } from '../package.json';
7
+ import { t } from './i18n';
8
+ import { when } from './when';
9
+
10
+ // Screen.tsx draws the status twice, but only one place shows at a width; both forms are here, switched with the M3 breakpoint
11
+ // classes (`expanded:`), never a width of their own.
12
+ export function StartStatus({ builtAt }: { builtAt: Date | null }) {
13
+ const cms = t.library.cmsVersion(shortVersion(version) as string);
14
+ const updated = builtAt ? t.library.siteUpdated(when(builtAt, { short: true })) : null;
15
+ return (
16
+ <>
17
+ <span className="expanded:inline hidden">
18
+ <CmsVersion />
19
+ {updated && `${t.library.statusSeparator}${updated}`}
20
+ </span>
21
+ <span className="expanded:hidden">{updated ? <SwapText text={updated} swapped={cms} /> : cms}</span>
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,22 @@
1
+ // A focusable span that swaps its text for another on a click or Enter/Space, and back (no link or button look: it inherits the text).
2
+ // Exposes SwapText. Depends on nothing.
3
+ import { useState } from 'react';
4
+
5
+ export function SwapText({ text, swapped, title }: { text: string; swapped: string; title?: string }) {
6
+ const [on, setOn] = useState(false);
7
+ return (
8
+ <span
9
+ tabIndex={0}
10
+ title={title}
11
+ onClick={() => setOn((was) => !was)}
12
+ onKeyDown={(e) => {
13
+ if (e.key === 'Enter' || e.key === ' ') {
14
+ e.preventDefault();
15
+ setOn((was) => !was);
16
+ }
17
+ }}
18
+ >
19
+ {on ? swapped : text}
20
+ </span>
21
+ );
22
+ }
package/src/allIcons.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  // All Phosphor's icons (regular) as text, by their file, for AnyIcon (icon.tsx): loaded only when it draws one the site
2
- // does not (an app's in the app store). The theme's are in their place, as for Svg (themePlugin, packages/editor/site/theme.mjs).
3
- export const allIcons = import.meta.glob<string>('/node_modules/@phosphor-icons/core/assets/regular/*.svg', {
4
- query: '?raw',
5
- import: 'default',
6
- eager: true,
7
- });
2
+ // does not (an app's in the app store). The theme's are in their place. One virtual module that themePlugin makes
3
+ // (allIconsModule, packages/editor/site/theme.mjs), so the build does not open 1500 files at once.
4
+ export { allIcons } from 'virtual:all-icons';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Provides a short form of the CMS version for the editor's start screen.
3
+ * Exposes the `shortVersion` function.
4
+ * Depends on nothing.
5
+ */
6
+ /** The CMS version as the start screen shows it: a pre-release without its build stamp, else the version as it is. */
7
+ export function shortVersion(version: any): any;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Provides a short form of the CMS version for the editor's start screen.
3
+ * Exposes the `shortVersion` function.
4
+ * Depends on nothing.
5
+ */
6
+
7
+ /** The CMS version as the start screen shows it: a pre-release without its build stamp, else the version as it is. */
8
+ export function shortVersion(version) {
9
+ const match = /^(\d+\.\d+\.\d+-[a-z]+)-\d+$/.exec(version);
10
+ return match ? match[1] : version;
11
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tests for the CMS version short form logic.
3
+ * Exposes test cases for `shortVersion`.
4
+ * Depends on `node:test`, `node:assert/strict`, and `./cmsVersionView.mjs`.
5
+ */
6
+ import { test } from 'node:test';
7
+ import * as assert from 'node:assert/strict';
8
+ import { shortVersion } from './cmsVersionView.mjs';
9
+
10
+ test('strips build stamp from pre-release versions', () => {
11
+ assert.equal(shortVersion('0.5.0-next-20261104200208'), '0.5.0-next');
12
+ });
13
+
14
+ test('strips build stamp from beta versions', () => {
15
+ assert.equal(shortVersion('1.2.3-beta-7'), '1.2.3-beta');
16
+ });
17
+
18
+ test('keeps stable versions unchanged', () => {
19
+ assert.equal(shortVersion('0.5.0'), '0.5.0');
20
+ });
21
+
22
+ test('keeps pre-release versions without build stamp unchanged', () => {
23
+ assert.equal(shortVersion('0.5.0-next'), '0.5.0-next');
24
+ });
25
+
26
+ test('keeps versions with non-numeric build metadata unchanged', () => {
27
+ assert.equal(shortVersion('0.5.0-rc.1'), '0.5.0-rc.1');
28
+ });
29
+
30
+ test('handles empty string', () => {
31
+ assert.equal(shortVersion(''), '');
32
+ });
@@ -2,6 +2,8 @@ import type { Messages } from '../en-US';
2
2
 
3
3
  export const library: Messages['library'] = {
4
4
  siteUpdated: (when) => `Opdateret ${when}`,
5
+ cmsVersion: (version) => `Morpheme ${version}`,
6
+ statusSeparator: ' · ',
5
7
  githubAccount: (login) => `GitHub-konto: ${login}`,
6
8
  logOut: 'Log ud',
7
9
  notifications: 'Notifikationer',
@@ -2,6 +2,10 @@
2
2
  // with choices (Sheet.tsx).
3
3
  export const library = {
4
4
  siteUpdated: (when: string) => `Updated ${when}`,
5
+ /** The CMS's name and version on the start screen (CmsVersion.tsx), before the update time. */
6
+ cmsVersion: (version: string) => `Morpheme ${version}`,
7
+ /** Between the CMS version and the update time in the start screen's status. */
8
+ statusSeparator: ' · ',
5
9
  githubAccount: (login: string) => `GitHub account: ${login}`,
6
10
  logOut: 'Log out',
7
11
  /** The bell in the top bar and its screen (#notifikationer, Notifications.tsx): the messages shown at the bottom. */
package/src/virtual.d.ts CHANGED
@@ -3,3 +3,8 @@ declare module 'virtual:app-folders' {
3
3
  const folders: Record<string, { files: string[]; csp?: Partial<Record<string, string[]>> }>;
4
4
  export default folders;
5
5
  }
6
+
7
+ // All Phosphor's icons (regular) as text, by their file (allIconsModule in site/theme.mjs).
8
+ declare module 'virtual:all-icons' {
9
+ export const allIcons: Record<string, string>;
10
+ }
@@ -31,7 +31,7 @@
31
31
  "@fontsource-variable/roboto": "^5.3.0",
32
32
  "@material/material-color-utilities": "^0.3.0",
33
33
  "@milkdown/kit": "^7.22.3",
34
- "@morphemecms/cms": "0.4.0",
34
+ "@morphemecms/cms": "0.5.0",
35
35
  "@phosphor-icons/core": "^2.1.1",
36
36
  "@phosphor-icons/react": "^2.1.10",
37
37
  "@tailwindcss/typography": "^0.5.20",
@@ -63,5 +63,10 @@
63
63
  "prettier-plugin-astro": "^1.1.2",
64
64
  "prettier-plugin-tailwindcss": "^0.8.1",
65
65
  "typescript": "^6.0.2"
66
+ },
67
+ "overrides": {
68
+ "@tailwindcss/typography": {
69
+ "postcss-selector-parser": "^6.1.5"
70
+ }
66
71
  }
67
72
  }
@@ -0,0 +1,2 @@
1
+ export const LOOKS: {};
2
+ export const HEADINGS: {};