@morphemecms/cms 0.2.0 → 0.3.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 +6 -0
- package/defaults/collections.ts +11 -0
- package/defaults/editor.ts +2 -0
- package/defaults/standardPages.ts +3 -17
- package/defaults/texts.ts +3 -0
- package/icons/lucide/calendar-x.svg +1 -0
- package/icons/lucide/identification-card.svg +1 -0
- package/icons/lucide/money.svg +1 -0
- package/icons/lucide/receipt.svg +1 -0
- package/icons/lucide/wallet.svg +1 -0
- package/icons/material-symbols/calendar-x.svg +1 -0
- package/icons/material-symbols/identification-card.svg +1 -0
- package/icons/material-symbols/money.svg +1 -0
- package/icons/material-symbols/receipt.svg +1 -0
- package/icons/material-symbols/wallet.svg +1 -0
- package/icons/phosphor/calendar-x.svg +1 -0
- package/icons/phosphor/identification-card.svg +1 -0
- package/icons/phosphor/money.svg +1 -0
- package/icons/phosphor/receipt.svg +1 -0
- package/icons/phosphor/wallet.svg +1 -0
- package/lib/side-block.d.mts +45 -0
- package/lib/side-block.mjs +75 -0
- package/lib/side-block.test.d.mts +1 -0
- package/lib/side-block.test.mjs +79 -0
- package/package.json +1 -1
- package/scripts/check-side.mjs +56 -0
- package/scripts/check-themes.mjs +15 -1
- package/scripts/editor-roundtrip.mjs +13 -2
- package/scripts/fixtures/side/block/View.tsx +9 -0
- package/scripts/fixtures/side/block/load.ts +8 -0
- package/scripts/fixtures/side/side-off.md +8 -0
- package/scripts/fixtures/side/side-plain.md +7 -0
- package/scripts/fixtures/side/side-wide.md +8 -0
- package/scripts/fixtures/side/side.json +3 -0
- package/scripts/icon-packs.mjs +3 -3
- package/site/content.config.ts +4 -0
- package/site/defineSite.d.mts +7 -1
- package/site/defineSite.mjs +7 -1
- package/site/icons.d.mts +21 -0
- package/site/icons.mjs +16 -1
- package/site/schema.ts +1 -1
- package/site/side.ts +10 -0
- package/site/site.ts +2 -0
- package/src/App.tsx +3 -1
- package/src/EntryForm.tsx +17 -6
- package/src/Library.tsx +9 -2
- package/src/SideColumn.tsx +83 -0
- package/src/frontmatter.d.mts +2 -0
- package/src/frontmatter.mjs +6 -0
- package/src/frontmatter.test.d.mts +1 -0
- package/src/frontmatter.test.mjs +27 -0
- package/src/i18n/da-DK/app.ts +1 -0
- package/src/i18n/da-DK/common.ts +1 -0
- package/src/i18n/da-DK/library.ts +9 -0
- package/src/i18n/en-US/app.ts +1 -0
- package/src/i18n/en-US/common.ts +2 -0
- package/src/i18n/en-US/library.ts +12 -0
- package/src/schema.ts +2 -0
- package/src/standard/Block.astro +7 -2
- package/src/standard/BlockSite.astro +2 -2
- package/src/standard/accordion/lead.ts +4 -0
- package/src/standard/cards/lead.ts +4 -0
- package/src/standard/frontLeads.ts +12 -0
- package/src/standard/hero/index.tsx +1 -1
- package/src/standard/hero/lead.ts +4 -0
- package/src/standard/leads.d.mts +8 -0
- package/src/standard/leads.mjs +14 -0
- package/src/standard/leads.test.d.mts +1 -0
- package/src/standard/leads.test.mjs +27 -0
- package/src/standard/posts/lead.ts +4 -0
- package/src/standard/steps/lead.ts +4 -0
- package/src/standard/testimonials/lead.ts +4 -0
- package/src/standard/view.tsx +5 -2
- package/src/standard/views.ts +8 -0
- package/theme/layouts/PageLayout.astro +19 -5
- package/theme/styles/components.css +23 -0
package/src/standard/Block.astro
CHANGED
|
@@ -11,9 +11,14 @@ interface Props {
|
|
|
11
11
|
props?: Record<string, unknown>;
|
|
12
12
|
/** Its shared texts, when the block takes them as RichText.astro gives them (blockTexts). */
|
|
13
13
|
texts?: Record<string, unknown>;
|
|
14
|
+
/** The page's address, and whether it is a new version, for a block that needs them (PageLayout.astro's column). */
|
|
15
|
+
path?: string;
|
|
16
|
+
preview?: boolean;
|
|
17
|
+
/** The page's text as HTML, for a block drawn beside it. */
|
|
18
|
+
html?: string;
|
|
14
19
|
}
|
|
15
|
-
const { type, props = {}, texts } = Astro.props;
|
|
20
|
+
const { type, props = {}, texts, path, preview, html } = Astro.props;
|
|
16
21
|
const Site = blockSites[type];
|
|
17
22
|
---
|
|
18
23
|
|
|
19
|
-
{Site && <Site type={type} props={props} texts={texts} />}
|
|
24
|
+
{Site && <Site type={type} props={props} path={path} preview={preview} texts={texts} html={html} />}
|
|
@@ -7,10 +7,10 @@ import type { SiteProps } from '../schema';
|
|
|
7
7
|
import { blockLoads, blockViews } from './views';
|
|
8
8
|
|
|
9
9
|
type Props = SiteProps & { type: string };
|
|
10
|
-
const { type, props, index, path, preview, texts } = Astro.props;
|
|
10
|
+
const { type, props, index, path, preview, texts, html } = Astro.props;
|
|
11
11
|
const contract = blockLoads[type];
|
|
12
12
|
const View = blockViews[type];
|
|
13
|
-
const view = contract && View ? await contract.load(props ?? {}, { texts: texts ?? {}, index, path, preview }) : null;
|
|
13
|
+
const view = contract && View ? await contract.load(props ?? {}, { texts: texts ?? {}, index, path, preview, html }) : null;
|
|
14
14
|
const after = view === null ? undefined : contract.after?.(view);
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The text above a block's card that is the block's own on the front page, from each block's lead.ts (the standard
|
|
2
|
+
// blocks' and the apps'). Exposes frontLeads (re-exported by defaults/standardPages.ts); depends on Vite's import.meta.glob.
|
|
3
|
+
import type { Lead } from '../schema';
|
|
4
|
+
import { leadsOf } from './leads.mjs';
|
|
5
|
+
|
|
6
|
+
type Found = Record<string, { lead: Lead }>;
|
|
7
|
+
|
|
8
|
+
/** The text above a block's card that is the block's own on the front page: its heading, text and picture (the top: the "Banner"'s text up to its "Knapper" block). */
|
|
9
|
+
export const frontLeads: Record<string, Lead> = leadsOf({
|
|
10
|
+
...import.meta.glob<{ lead: Lead }>('./*/lead.ts', { eager: true }),
|
|
11
|
+
...(import.meta.glob<{ lead: Lead }>('/src/apps/*/blocks/*/lead.ts', { eager: true }) as Found),
|
|
12
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// page's H1), the first paragraph the subtitle, the others the introduction, and the text after the buttons is under
|
|
4
4
|
// them, a quote ("> …") in it too (fields.ts); the picture beside the text or behind it (`placement`), across the whole
|
|
5
5
|
// width of the window or as wide as the text (`width`); at the top of a page or anywhere in its text. At the top of the
|
|
6
|
-
// front page its text up to the buttons is written as the text above its card (`
|
|
6
|
+
// front page its text up to the buttons is written as the text above its card (`lead.ts` in this folder,
|
|
7
7
|
// sections.mjs). How it looks is the theme's
|
|
8
8
|
// (packages/editor/theme/blocks/hero/View.tsx; what it shows: view.ts, and load.ts on the site).
|
|
9
9
|
import { ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Columns, ImageSquare, Sparkle } from '@phosphor-icons/react';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Record<string, { lead: import('../schema').Lead }>} modules glob results, keyed by the path of each lead.ts
|
|
3
|
+
* @returns {Record<string, import('../schema').Lead>} the leads, keyed by block type (the folder of lead.ts);
|
|
4
|
+
* an app's lead (a path under /apps/) over a standard one of the same type, whatever the order of the keys
|
|
5
|
+
*/
|
|
6
|
+
export function leadsOf(modules: Record<string, {
|
|
7
|
+
lead: any;
|
|
8
|
+
}>): Record<string, any>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The front page's leads of the blocks (lead.ts in a block's folder), keyed by block type: what leadsOf makes of the
|
|
2
|
+
// glob results of the standard blocks and the apps' blocks. Exposes leadsOf; used by frontLeads.ts. No imports.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Record<string, { lead: import('../schema').Lead }>} modules glob results, keyed by the path of each lead.ts
|
|
6
|
+
* @returns {Record<string, import('../schema').Lead>} the leads, keyed by block type (the folder of lead.ts);
|
|
7
|
+
* an app's lead (a path under /apps/) over a standard one of the same type, whatever the order of the keys
|
|
8
|
+
*/
|
|
9
|
+
export function leadsOf(modules) {
|
|
10
|
+
const own = (path) => path.includes('/apps/');
|
|
11
|
+
const entries = Object.entries(modules).map(([path, m]) => ({ path, type: path.split('/').at(-2), lead: m.lead }));
|
|
12
|
+
const ordered = [...entries.filter((e) => !own(e.path)), ...entries.filter((e) => own(e.path))];
|
|
13
|
+
return Object.fromEntries(ordered.map((e) => [e.type, e.lead]));
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Tests leadsOf (leads.mjs): the front-page leads of the blocks, keyed by block type, an app's over a standard one.
|
|
2
|
+
// Run: node --test src/standard/leads.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { leadsOf } from './leads.mjs';
|
|
6
|
+
|
|
7
|
+
test('keys a standard lead by its block type', () => {
|
|
8
|
+
assert.deepEqual(leadsOf({ '/src/standard/hero/lead.ts': { lead: { body: 'text', upTo: 'buttons' } } }), {
|
|
9
|
+
hero: { body: 'text', upTo: 'buttons' },
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('keys an app lead by its block type', () => {
|
|
14
|
+
const found = leadsOf({ '/src/apps/x/blocks/prices/lead.ts': { lead: { heading: 'heading' } } });
|
|
15
|
+
assert.deepEqual(Object.keys(found), ['prices']);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('an app lead wins over a standard one, in either order', () => {
|
|
19
|
+
const standard = ['./hero/lead.ts', { lead: { heading: 'standard' } }];
|
|
20
|
+
const app = ['/src/apps/x/blocks/hero/lead.ts', { lead: { heading: 'app' } }];
|
|
21
|
+
assert.deepEqual(leadsOf(Object.fromEntries([standard, app])), { hero: { heading: 'app' } });
|
|
22
|
+
assert.deepEqual(leadsOf(Object.fromEntries([app, standard])), { hero: { heading: 'app' } });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('no modules, no leads', () => {
|
|
26
|
+
assert.deepEqual(leadsOf({}), {});
|
|
27
|
+
});
|
package/src/standard/view.tsx
CHANGED
|
@@ -76,8 +76,9 @@ export const shownPicture = (src: string): Picture => ({ src: shownAt(src) });
|
|
|
76
76
|
export type ViewOf<V> = (props: Record<string, any>, data: BlockData) => V | null;
|
|
77
77
|
|
|
78
78
|
/** Where a block stands on the site, for its load.ts: its shared texts (shared.ts, with those saved over them), its
|
|
79
|
-
* place among the parts of the text, the page's address,
|
|
80
|
-
|
|
79
|
+
* place among the parts of the text, the page's address, whether the page is a new version (/ny/), and, for a block
|
|
80
|
+
* drawn beside the page's text (`beside`), the text as HTML. */
|
|
81
|
+
export type LoadContext = { texts: Record<string, any>; index: number; path?: string; preview?: boolean; html?: string };
|
|
81
82
|
|
|
82
83
|
/** What a block's contract makes its view from on the site, when it is built (load.ts). Null: nothing is shown. */
|
|
83
84
|
export type Load<V> = (props: Record<string, any>, at: LoadContext) => Promise<V | null>;
|
|
@@ -89,6 +90,8 @@ export type Contract<V> = {
|
|
|
89
90
|
after?: (view: V) => string | undefined;
|
|
90
91
|
/** Drawn across the page, below the text and the column beside it, when it ends a page's text (endBlock in views.ts). */
|
|
91
92
|
across?: boolean;
|
|
93
|
+
/** Can be drawn in a column beside the text of a page, when src/data/side.json chooses its type (sideBlock in views.ts). */
|
|
94
|
+
beside?: boolean;
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
/** The kinds of a button, as Material 3 names them (m3.material.io/components/buttons): what a block's or an app's
|
package/src/standard/views.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { appLooks, themeViews } from './siteLooks';
|
|
5
5
|
import type { Contract, View } from './view';
|
|
6
6
|
import { parseBlock } from '../../lib/blocks.mjs';
|
|
7
|
+
import { sideBlockOf } from '../../lib/side-block.mjs';
|
|
8
|
+
import { sideSetting } from '../../site/side';
|
|
7
9
|
|
|
8
10
|
const byType = <T>(found: Record<string, T>) => Object.fromEntries(Object.entries(found).map(([file, m]) => [file.split('/').at(-2)!, m]));
|
|
9
11
|
const defaults = <T>(found: Record<string, { default: T }>) =>
|
|
@@ -22,6 +24,12 @@ export const blockLoads: Record<string, Contract<any>> = {
|
|
|
22
24
|
...byType(appLooks.loads),
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
/** The block type drawn in a column beside the text of a page, or null: the one src/data/side.json chooses, when its
|
|
28
|
+
* load.ts says `beside`, unless the page is `wide` or has `side: false` (lib/side-block.mjs). The theme's page layout
|
|
29
|
+
* (PageLayout.astro) draws it with Block.astro, given the page's text as `html`. */
|
|
30
|
+
export const sideBlock = (page: { wide?: boolean; side?: boolean }): string | null =>
|
|
31
|
+
sideBlockOf(page, sideSetting, (type) => blockLoads[type]?.beside === true);
|
|
32
|
+
|
|
25
33
|
// A block's tag in the text, at its very end (blocks.mjs writes it so).
|
|
26
34
|
const END = /<div data-block="([\w-]+)"(?: data-props="[^"]*")?\s*><\/div>\s*$/;
|
|
27
35
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
// A page (the standard theme's): back link, title and the text with its blocks (its pictures in it), in one column
|
|
2
|
+
// A page (the standard theme's): back link, title and the text with its blocks (its pictures in it), in one column (with
|
|
3
|
+
// the block src/data/side.json chooses in a column beside it, when there is one: sideBlock), and the booking
|
|
3
4
|
// prompt across the page when the text ends with it. A wide page (`wide`, "Hele bredden"): its blocks go across the
|
|
4
5
|
// page, its text keeps a readable width, and its first paragraph is the introduction under the heading (as
|
|
5
6
|
// PageHeader.astro's).
|
|
@@ -8,9 +9,10 @@
|
|
|
8
9
|
import Base from '@theme/layouts/Base.astro';
|
|
9
10
|
import Icon from '@theme/components/Icon.astro';
|
|
10
11
|
import EndBlock from '@theme/components/EndBlock.astro';
|
|
11
|
-
import
|
|
12
|
+
import Block from '@morphemecms/cms/src/standard/Block.astro';
|
|
13
|
+
import { blockLoads, endBlock, sideBlock } from '@morphemecms/cms/src/standard/views';
|
|
12
14
|
import { getPages, pageHeading, pageHeadingHtml, parentPage, stateOf, withoutH1, type Page } from '@morphemecms/cms/site/content';
|
|
13
|
-
import { nav } from '@morphemecms/cms/site/site';
|
|
15
|
+
import { blockTexts, nav } from '@morphemecms/cms/site/site';
|
|
14
16
|
import { firstPicture, picturesOf } from '@morphemecms/cms/site/images.mjs';
|
|
15
17
|
import { splitBlocks } from '@morphemecms/cms/lib/blocks.mjs';
|
|
16
18
|
import { markdownHtml } from '@morphemecms/cms/lib/inline';
|
|
@@ -61,6 +63,13 @@ const questions = await Promise.all(
|
|
|
61
63
|
const preview = isPreview(path);
|
|
62
64
|
const current = livePath(path);
|
|
63
65
|
const review = (page.data.review ?? '').trim();
|
|
66
|
+
// The block beside the text (sideBlock): when its type's load gives a view; else the page is drawn as without one.
|
|
67
|
+
const sideType = sideBlock(page.data);
|
|
68
|
+
const sideText = end ? end.html : html;
|
|
69
|
+
const side =
|
|
70
|
+
sideType && (await blockLoads[sideType].load({}, { texts: blockTexts[sideType] ?? {}, index: 0, path: current, preview, html: sideText })) !== null
|
|
71
|
+
? sideType
|
|
72
|
+
: null;
|
|
64
73
|
---
|
|
65
74
|
|
|
66
75
|
<Base
|
|
@@ -93,7 +102,7 @@ const review = (page.data.review ?? '').trim();
|
|
|
93
102
|
</Container>
|
|
94
103
|
|
|
95
104
|
<Container as="div" class:list={[wide ? 'mt-4' : 'mt-8 md:mt-12']}>
|
|
96
|
-
<div>
|
|
105
|
+
<div {...(side ? { class: 'page-with-side' } : {})}>
|
|
97
106
|
<div
|
|
98
107
|
data-editor="text"
|
|
99
108
|
class:list={[
|
|
@@ -103,8 +112,13 @@ const review = (page.data.review ?? '').trim();
|
|
|
103
112
|
: 'max-w-[66ch]',
|
|
104
113
|
]}
|
|
105
114
|
>
|
|
106
|
-
<RichText html={
|
|
115
|
+
<RichText html={sideText} path={current} preview={preview} />
|
|
107
116
|
</div>
|
|
117
|
+
{side && (
|
|
118
|
+
<aside class="page-side">
|
|
119
|
+
<Block type={side} texts={blockTexts[side]} path={current} preview={preview} html={sideText} />
|
|
120
|
+
</aside>
|
|
121
|
+
)}
|
|
108
122
|
</div>
|
|
109
123
|
</Container>
|
|
110
124
|
</article>
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
/* The space over the first block on the blog page, under the tags: its posts, close to them. */
|
|
21
21
|
--theme-blog-first-space: 2.5rem;
|
|
22
22
|
--theme-blog-first-space-large: 3.5rem;
|
|
23
|
+
/* The column beside the text of a page (PageLayout.astro, the block src/data/side.json chooses): its width and the gap
|
|
24
|
+
between it and the text, from md. */
|
|
25
|
+
--theme-side-width: 20rem;
|
|
26
|
+
--theme-side-gap: 3rem;
|
|
23
27
|
/* The logo (Logo.astro, and at the top of the menu on a phone): the mark (the site's logo) in a tile, its colour and
|
|
24
28
|
size (a share of the mark's own), the name's weight and size, and the role (hidden with none; on a phone, and from md). */
|
|
25
29
|
--theme-logo-tile: transparent;
|
|
@@ -189,6 +193,25 @@
|
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
|
|
196
|
+
/* A page with a block beside its text (PageLayout.astro): the text, then the column (on a phone below the text); from md
|
|
197
|
+
two columns, the second one staying in view while the text is read. */
|
|
198
|
+
.page-with-side > .page-side {
|
|
199
|
+
margin-top: 3rem;
|
|
200
|
+
}
|
|
201
|
+
@media (min-width: 48rem) {
|
|
202
|
+
.page-with-side {
|
|
203
|
+
display: grid;
|
|
204
|
+
grid-template-columns: minmax(0, 1fr) var(--theme-side-width);
|
|
205
|
+
column-gap: var(--theme-side-gap);
|
|
206
|
+
align-items: start;
|
|
207
|
+
}
|
|
208
|
+
.page-with-side > .page-side {
|
|
209
|
+
margin-top: 0;
|
|
210
|
+
position: sticky;
|
|
211
|
+
top: 6rem;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
/* A page's and a post's title (PageHeader.astro, PageLayout.astro, PostLayout.astro, Home.astro): the display role, by
|
|
193
216
|
the variables above. */
|
|
194
217
|
.theme-title {
|