@morphemecms/cms 0.7.1 → 0.8.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 +12 -0
- package/defaults/collections.ts +56 -0
- package/defaults/company.ts +13 -4
- package/defaults/texts.ts +17 -0
- package/icons/lucide/LICENSE-phosphor +21 -0
- package/icons/lucide/facebook-logo.svg +1 -0
- package/icons/lucide/linkedin-logo.svg +1 -0
- package/icons/lucide/pack.json +16 -1
- package/icons/lucide/pinterest-logo.svg +1 -0
- package/icons/lucide/x-logo.svg +1 -0
- package/icons/material-symbols/LICENSE-phosphor +21 -0
- package/icons/material-symbols/facebook-logo.svg +1 -0
- package/icons/material-symbols/linkedin-logo.svg +1 -0
- package/icons/material-symbols/pack.json +15 -0
- package/icons/material-symbols/pinterest-logo.svg +1 -0
- package/icons/material-symbols/x-logo.svg +1 -0
- package/icons/phosphor/facebook-logo.svg +1 -0
- package/icons/phosphor/linkedin-logo.svg +1 -0
- package/icons/phosphor/pinterest-logo.svg +1 -0
- package/icons/phosphor/x-logo.svg +1 -0
- package/lib/lead.d.mts +5 -0
- package/lib/lead.mjs +10 -0
- package/lib/lead.test.mjs +65 -0
- package/package.json +2 -1
- package/scripts/icon-packs.mjs +51 -13
- package/scripts/material-fallback.test.mjs +49 -0
- package/site/icons.d.mts +13 -0
- package/site/icons.mjs +25 -3
- package/site/schema.ts +2 -0
- package/site/site.ts +9 -2
- package/site/social.d.mts +37 -0
- package/site/social.mjs +92 -0
- package/site/social.test.mjs +80 -0
- package/site/theme.d.mts +2 -0
- package/site/theme.mjs +24 -10
- package/site/themeImport.test.mjs +40 -0
- package/src/EntryForm.tsx +21 -58
- package/src/FormFrame.tsx +5 -5
- package/src/Parts.tsx +8 -30
- package/src/Screen.tsx +12 -5
- package/src/editor.css +70 -5
- package/src/i18n/da-DK/parts.ts +1 -1
- package/src/i18n/da-DK/theme.ts +4 -0
- package/src/i18n/en-US/parts.ts +1 -1
- package/src/i18n/en-US/theme.ts +4 -0
- package/src/icon.tsx +1 -1
- package/src/pagePreview.tsx +21 -1
- package/src/postLead.d.mts +7 -0
- package/src/postLead.mjs +9 -0
- package/src/postLead.test.mjs +28 -0
- package/src/ui.tsx +41 -96
- package/starter/package.json +1 -1
- package/starter/src/data/social.json +14 -0
- package/theme/components/PostAuthor.astro +20 -0
- package/theme/components/PostCard.tsx +26 -0
- package/theme/components/PostHeader.astro +36 -0
- package/theme/components/PostMeta.tsx +27 -2
- package/theme/components/RelatedPosts.astro +29 -0
- package/theme/components/ShareRow.astro +48 -0
- package/theme/layouts/PostLayout.astro +28 -54
- package/theme/styles/base.css +164 -127
- package/theme/styles/blocks.css +160 -155
- package/theme/styles/components.css +43 -34
- package/theme/styles/global.css +316 -303
- package/theme/styles/ui.css +43 -34
- package/theme/ui/copyLink.script.ts +17 -0
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
// Date and reading time for a blog post: "12. marts 2024 · 5 min. læsetid" in Danish
|
|
1
|
+
// Date and reading time for a blog post, then its tags as links: "12. marts 2024 · 5 min. læsetid · News, Tips" in Danish
|
|
2
|
+
// (posts.ts makes them). Props: post, optional tags and className. A theme may replace this file.
|
|
3
|
+
import { Fragment } from 'react';
|
|
2
4
|
import { Svg } from '@morphemecms/cms/src/icon';
|
|
3
5
|
import type { PostView } from '@theme/components/posts';
|
|
4
6
|
import { siteT } from '@morphemecms/cms/src/i18n';
|
|
5
7
|
|
|
6
|
-
export default function PostMeta({
|
|
8
|
+
export default function PostMeta({
|
|
9
|
+
post,
|
|
10
|
+
tags,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
post: PostView;
|
|
14
|
+
tags?: { label: string; href: string }[];
|
|
15
|
+
className?: string;
|
|
16
|
+
}) {
|
|
7
17
|
return (
|
|
8
18
|
<p className={['flex flex-wrap items-center gap-x-2 type-body-medium text-muted', className].filter(Boolean).join(' ')}>
|
|
9
19
|
<time dateTime={post.date}>{post.dateText}</time>
|
|
@@ -12,6 +22,21 @@ export default function PostMeta({ post, className }: { post: PostView; classNam
|
|
|
12
22
|
<Svg name="clock" size={15} />
|
|
13
23
|
{siteT.theme.readingTime(post.minutes)}
|
|
14
24
|
</span>
|
|
25
|
+
{tags && tags.length > 0 && (
|
|
26
|
+
<>
|
|
27
|
+
<span aria-hidden="true">·</span>
|
|
28
|
+
<span>
|
|
29
|
+
{tags.map((tag, i) => (
|
|
30
|
+
<Fragment key={tag.href}>
|
|
31
|
+
{i > 0 && ', '}
|
|
32
|
+
<a href={tag.href} className="underline hover:text-accent">
|
|
33
|
+
{tag.label}
|
|
34
|
+
</a>
|
|
35
|
+
</Fragment>
|
|
36
|
+
))}
|
|
37
|
+
</span>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
15
40
|
</p>
|
|
16
41
|
);
|
|
17
42
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A "Related posts" section under a blog post: up to three PostCard components in a responsive grid. A theme may replace
|
|
3
|
+
// this file to redesign the section.
|
|
4
|
+
import Container from '@theme/components/Container.astro';
|
|
5
|
+
import PostCard from '@theme/components/PostCard';
|
|
6
|
+
import { siteT } from '@morphemecms/cms/src/i18n';
|
|
7
|
+
import type { PostView } from '@theme/components/posts';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
posts: PostView[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { posts } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{posts.length > 0 && (
|
|
17
|
+
<Container class="mt-12 medium:mt-16" aria-labelledby="related-posts">
|
|
18
|
+
<h2 id="related-posts" class="type-headline-small">
|
|
19
|
+
{siteT.theme.relatedPosts}
|
|
20
|
+
</h2>
|
|
21
|
+
<ul class="mt-6 grid gap-6 medium:grid-cols-3">
|
|
22
|
+
{posts.map((post) => (
|
|
23
|
+
<li>
|
|
24
|
+
<PostCard post={post} />
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
</Container>
|
|
29
|
+
)}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The share buttons under a blog post: plain links to Facebook, X, LinkedIn and Pinterest, and a button that copies the
|
|
3
|
+
// post's address (theme/ui/copyLink.script.ts). Props: the post's address, title and picture, and which buttons are on
|
|
4
|
+
// (site.social.share). Draws nothing without buttons. Uses shareLinks (site/social.mjs), Icon and IconButton's classes.
|
|
5
|
+
import { shareLinks, type ShareId } from '@morphemecms/cms/site/social.mjs';
|
|
6
|
+
import Icon from '@theme/components/Icon.astro';
|
|
7
|
+
import { iconButtonClass } from '@theme/ui/IconButton';
|
|
8
|
+
import { siteT } from '@morphemecms/cms/src/i18n';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
url: string;
|
|
12
|
+
title: string;
|
|
13
|
+
picture?: string;
|
|
14
|
+
share: ShareId[];
|
|
15
|
+
}
|
|
16
|
+
const { url, title, picture, share } = Astro.props;
|
|
17
|
+
|
|
18
|
+
const links = shareLinks({ url, title, picture }, share);
|
|
19
|
+
const networks: Record<string, { name: string; icon: string }> = {
|
|
20
|
+
facebook: { name: 'Facebook', icon: 'facebook-logo' },
|
|
21
|
+
x: { name: 'X', icon: 'x-logo' },
|
|
22
|
+
linkedin: { name: 'LinkedIn', icon: 'linkedin-logo' },
|
|
23
|
+
pinterest: { name: 'Pinterest', icon: 'pinterest-logo' },
|
|
24
|
+
};
|
|
25
|
+
const buttonClass = iconButtonClass('standard');
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{links.length > 0 && (
|
|
29
|
+
<div role="group" aria-label={siteT.theme.share} class="flex items-center gap-x-2">
|
|
30
|
+
{links.map(({ id, href }) =>
|
|
31
|
+
id === 'copy-link' ? (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class={buttonClass}
|
|
35
|
+
aria-label={siteT.theme.copyLink}
|
|
36
|
+
data-copy-link={url}
|
|
37
|
+
data-copied={siteT.theme.linkCopied}
|
|
38
|
+
>
|
|
39
|
+
<Icon name="link-simple" size={22} />
|
|
40
|
+
</button>
|
|
41
|
+
) : (
|
|
42
|
+
<a href={href} class={buttonClass} aria-label={siteT.theme.shareOn(networks[id].name)} target="_blank" rel="noopener">
|
|
43
|
+
<Icon name={networks[id].icon} size={22} />
|
|
44
|
+
</a>
|
|
45
|
+
),
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
// Blog post template. A narrow reading column, the site's owner as author
|
|
2
|
+
// Blog post template. A narrow reading column, the site's owner as author. Order: PostHeader (back link, title, author,
|
|
3
|
+
// byline with tags, share row), the lead (the text's first paragraph), the featured picture, the rest of the text, the
|
|
4
|
+
// end block, RelatedPosts. Without a lead the picture comes before the text.
|
|
3
5
|
// data-editor marks what the editor's preview fills in with what is in its form ("Forhåndsvis",
|
|
4
|
-
// packages/editor/src/pagePreview.tsx): the heading, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
|
|
6
|
+
// packages/editor/src/pagePreview.tsx): the heading, the lead, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
|
|
7
|
+
import { splitLead } from '@morphemecms/cms/lib/lead.mjs';
|
|
5
8
|
import { firstImage, heroImage, picturesOf } from '@morphemecms/cms/site/images.mjs';
|
|
6
9
|
import Base from '@theme/layouts/Base.astro';
|
|
7
|
-
import Icon from '@theme/components/Icon.astro';
|
|
8
10
|
import Img from '@theme/components/Img.astro';
|
|
9
11
|
import EndBlock from '@theme/components/EndBlock.astro';
|
|
10
12
|
import RichText from '@theme/components/RichText.astro';
|
|
11
13
|
import { categoryName, getPosts, postPath, stateOf, type Post } from '@morphemecms/cms/site/content';
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
+
import PostHeader from '@theme/components/PostHeader.astro';
|
|
15
|
+
import RelatedPosts from '@theme/components/RelatedPosts.astro';
|
|
14
16
|
import { postViewOf } from '@theme/components/loadPosts';
|
|
15
17
|
import { site } from '@morphemecms/cms/site/site';
|
|
16
18
|
import { isBlogCategory } from '@morphemecms/cms/site/categories';
|
|
@@ -24,11 +26,20 @@ const { post } = Astro.props;
|
|
|
24
26
|
const { title, description, date, updated, categories } = post.data;
|
|
25
27
|
// A decorative picture (imageDecorative) has no alternative text.
|
|
26
28
|
const imageAlt = post.data.imageDecorative ? undefined : post.data.imageAlt;
|
|
27
|
-
// The
|
|
29
|
+
// The tags in the byline: only the blog categories (the others have no page).
|
|
28
30
|
const onBlog = categories.filter(isBlogCategory);
|
|
29
31
|
// Some posts also have their featured image (or another version of it) inside the text; never show it twice.
|
|
30
32
|
const image = heroImage(post.data.image, post.body);
|
|
31
33
|
|
|
34
|
+
// The picture a share to Pinterest pins: the one chosen for sharing, else the post's own, with the site's address in front.
|
|
35
|
+
const sharePicture = post.data.shareImage || post.data.image || firstImage(post.body);
|
|
36
|
+
const picture = sharePicture?.startsWith('/') ? site.url + sharePicture : sharePicture || undefined;
|
|
37
|
+
|
|
38
|
+
// The text's first paragraph is the lead, drawn before the featured picture; without one the picture comes first.
|
|
39
|
+
const { lead, rest } = splitLead(await Astro.slots.render('default'));
|
|
40
|
+
// The lead has the text's class too (the editor's preview copies the text's class to the lead) and post-lead.
|
|
41
|
+
const textClass = 'prose mx-auto mt-10 max-w-3xl px-5 medium:mt-14 medium:px-8';
|
|
42
|
+
|
|
32
43
|
// Related: most shared categories first, then newest.
|
|
33
44
|
const related = (await getPosts())
|
|
34
45
|
.filter((p) => p.id !== post.id)
|
|
@@ -69,23 +80,14 @@ const related = (await getPosts())
|
|
|
69
80
|
]}
|
|
70
81
|
>
|
|
71
82
|
<article>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{site.portrait && (
|
|
81
|
-
<Img src={site.portrait} alt="" loading="eager" class="size-11 rounded-full object-cover object-[50%_20%]" sizes="44px" />
|
|
82
|
-
)}
|
|
83
|
-
<div class="leading-snug">
|
|
84
|
-
<span class="block font-medium">{site.name}</span>
|
|
85
|
-
<PostMeta post={postViewOf(post)} />
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</header>
|
|
83
|
+
<PostHeader
|
|
84
|
+
title={title}
|
|
85
|
+
post={postViewOf(post)}
|
|
86
|
+
tags={onBlog.map((c) => ({ label: categoryName(c), href: `/blog/kategori/${c}/` }))}
|
|
87
|
+
share={{ url: site.url + postPath(post), title, picture, share: site.social.share }}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
{lead && <div data-editor="lead" class={`${textClass} post-lead`} set:html={lead} />}
|
|
89
91
|
|
|
90
92
|
{image && (
|
|
91
93
|
<div data-editor="image" class="mx-auto mt-8 max-w-5xl px-5 medium:mt-12 medium:px-8">
|
|
@@ -99,40 +101,12 @@ const related = (await getPosts())
|
|
|
99
101
|
</div>
|
|
100
102
|
)}
|
|
101
103
|
|
|
102
|
-
<div data-editor="text" class=
|
|
103
|
-
<RichText html={
|
|
104
|
+
<div data-editor="text" class={textClass}>
|
|
105
|
+
<RichText html={rest} blocks={false} />
|
|
104
106
|
</div>
|
|
105
|
-
|
|
106
|
-
<footer class="mx-auto mt-12 max-w-3xl px-5 medium:px-8">
|
|
107
|
-
{onBlog.length > 0 && (
|
|
108
|
-
<ul class="flex flex-wrap gap-2" aria-label="Tags">
|
|
109
|
-
{onBlog.map((c) => (
|
|
110
|
-
<li>
|
|
111
|
-
<Chip href={`/blog/kategori/${c}/`} label={categoryName(c)} />
|
|
112
|
-
</li>
|
|
113
|
-
))}
|
|
114
|
-
</ul>
|
|
115
|
-
)}
|
|
116
|
-
</footer>
|
|
117
107
|
</article>
|
|
118
108
|
|
|
119
109
|
{site.postEnd && <EndBlock type={site.postEnd} />}
|
|
120
110
|
|
|
121
|
-
{related.
|
|
122
|
-
<Fragment slot="footer-column">
|
|
123
|
-
<p class="footer-heading font-medium">{siteT.theme.relatedPosts}</p>
|
|
124
|
-
<ul class="mt-3 space-y-2.5 text-muted">
|
|
125
|
-
{related.map((p) => (
|
|
126
|
-
<li>
|
|
127
|
-
<a href={postPath(p)} class="hover:text-accent">
|
|
128
|
-
{p.data.title}
|
|
129
|
-
</a>
|
|
130
|
-
</li>
|
|
131
|
-
))}
|
|
132
|
-
</ul>
|
|
133
|
-
<a href="/blog/" class="mt-4 inline-flex items-center gap-1.5 font-medium text-accent">
|
|
134
|
-
{siteT.theme.seeAll} <Icon name="arrow-right" size={16} />
|
|
135
|
-
</a>
|
|
136
|
-
</Fragment>
|
|
137
|
-
)}
|
|
111
|
+
<RelatedPosts posts={related.map(postViewOf)} />
|
|
138
112
|
</Base>
|
package/theme/styles/base.css
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
2
|
@plugin '@tailwindcss/typography';
|
|
3
|
+
/* Material's rules sit in the last layer, fallback: a theme's own unlayered rules beat them, whatever their specificity. */
|
|
4
|
+
@layer theme, base, components, utilities, fallback;
|
|
3
5
|
/* The theme's UI components, as Material 3 names them (buttons, icon buttons), and the standard blocks of the editor,
|
|
4
6
|
in this site's design (their variables below). */
|
|
5
7
|
@import './ui.css';
|
|
@@ -8,6 +10,9 @@
|
|
|
8
10
|
@import './components.css';
|
|
9
11
|
|
|
10
12
|
/*
|
|
13
|
+
Material is every theme's fallback: its rules that set more than custom properties sit in @layer fallback (declared last in
|
|
14
|
+
base.css), so a theme's own unlayered rules win over them, whatever the specificity. Custom properties stay unlayered.
|
|
15
|
+
|
|
11
16
|
The standard theme's base: what every theme builds on, plain and neutral. The system's own font, a small radius on
|
|
12
17
|
panels, a light shadow, full pills on controls; the classes the standard theme's components use (.link, .prose,
|
|
13
18
|
the motion …) besides the UI components (ui.css: buttons, icon buttons, chips, snackbars). The standard theme's own look is over it, in global.css; a theme with a look of its
|
|
@@ -31,7 +36,6 @@
|
|
|
31
36
|
--shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 8px 24px -12px rgb(0 0 0 / 0.18);
|
|
32
37
|
/* The motion scheme's curve (blocks.css, lib/motion.ts). */
|
|
33
38
|
--ease: var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
|
|
34
|
-
color-scheme: light;
|
|
35
39
|
|
|
36
40
|
/* The standard blocks (blocks.css) in this design; in dark mode too, as they
|
|
37
41
|
follow the tokens above. */
|
|
@@ -53,17 +57,34 @@
|
|
|
53
57
|
/* The same for a chip set that scrolls (ui.css). */
|
|
54
58
|
--chip-set-bleed: 1.25rem;
|
|
55
59
|
}
|
|
60
|
+
@layer fallback {
|
|
61
|
+
:root {
|
|
62
|
+
color-scheme: light;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
56
65
|
|
|
57
66
|
@media (prefers-color-scheme: dark) {
|
|
58
67
|
:root:not([data-theme='light']) {
|
|
59
68
|
--shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 20px 40px -24px rgb(0 0 0 / 0.7);
|
|
60
|
-
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@layer fallback {
|
|
73
|
+
@media (prefers-color-scheme: dark) {
|
|
74
|
+
:root:not([data-theme='light']) {
|
|
75
|
+
color-scheme: dark;
|
|
76
|
+
}
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
:root[data-theme='dark'] {
|
|
65
81
|
--shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 20px 40px -24px rgb(0 0 0 / 0.7);
|
|
66
|
-
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@layer fallback {
|
|
85
|
+
:root[data-theme='dark'] {
|
|
86
|
+
color-scheme: dark;
|
|
87
|
+
}
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
@theme inline {
|
|
@@ -176,152 +197,168 @@
|
|
|
176
197
|
--tw-prose-th-borders: var(--line);
|
|
177
198
|
--tw-prose-td-borders: var(--line);
|
|
178
199
|
--tw-prose-code: var(--ink);
|
|
179
|
-
font-size: var(--type-body-large-size);
|
|
180
|
-
line-height: var(--type-body-large-line-height);
|
|
181
|
-
font-weight: var(--type-body-large-weight);
|
|
182
|
-
letter-spacing: var(--type-body-large-tracking);
|
|
183
|
-
}
|
|
184
|
-
/* The text's headings: a headline, a title under it and a smaller title under that (the type scale, ui.css). */
|
|
185
|
-
.prose :where(h2) {
|
|
186
|
-
font-size: var(--type-headline-medium-size);
|
|
187
|
-
line-height: var(--type-headline-medium-line-height);
|
|
188
|
-
font-weight: var(--type-headline-medium-weight);
|
|
189
|
-
letter-spacing: var(--type-headline-medium-tracking);
|
|
190
|
-
}
|
|
191
|
-
.prose :where(h3) {
|
|
192
|
-
font-size: var(--type-title-large-size);
|
|
193
|
-
line-height: var(--type-title-large-line-height);
|
|
194
|
-
font-weight: var(--type-title-large-weight);
|
|
195
|
-
letter-spacing: var(--type-title-large-tracking);
|
|
196
|
-
}
|
|
197
|
-
.prose :where(h4) {
|
|
198
|
-
font-size: var(--type-title-medium-size);
|
|
199
|
-
line-height: var(--type-title-medium-line-height);
|
|
200
|
-
font-weight: var(--type-title-medium-weight);
|
|
201
|
-
letter-spacing: var(--type-title-medium-tracking);
|
|
202
|
-
}
|
|
203
|
-
/* Code in a line (cookie names in the privacy policy): on a tint, without the plugin's backticks. */
|
|
204
|
-
.prose :where(:not(pre) > code):not(:where(.not-prose code)) {
|
|
205
|
-
background: var(--tint);
|
|
206
|
-
border-radius: 0.3em;
|
|
207
|
-
padding: 0.1em 0.35em;
|
|
208
|
-
font-weight: 500;
|
|
209
200
|
}
|
|
210
|
-
|
|
211
|
-
.prose
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
text-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
201
|
+
@layer fallback {
|
|
202
|
+
.prose {
|
|
203
|
+
font-size: var(--type-body-large-size);
|
|
204
|
+
line-height: var(--type-body-large-line-height);
|
|
205
|
+
font-weight: var(--type-body-large-weight);
|
|
206
|
+
letter-spacing: var(--type-body-large-tracking);
|
|
207
|
+
}
|
|
208
|
+
/* A blog post's lead: its first paragraph, one step larger than the text (the title-large size, with the body weight). */
|
|
209
|
+
.prose.post-lead {
|
|
210
|
+
font-size: var(--type-title-large-size);
|
|
211
|
+
line-height: var(--type-title-large-line-height);
|
|
212
|
+
font-weight: var(--type-body-large-weight);
|
|
213
|
+
letter-spacing: var(--type-title-large-tracking);
|
|
214
|
+
}
|
|
215
|
+
/* The text right after the lead (no picture between): the space the prose puts between paragraphs (1.25em), not the
|
|
216
|
+
larger space of a text under a picture. Unlayered on purpose: it must beat the mt-10 / medium:mt-14 utilities (a layer). */
|
|
217
|
+
.prose.post-lead + .prose {
|
|
218
|
+
margin-top: 1.25em;
|
|
219
|
+
}
|
|
220
|
+
/* The text's headings: a headline, a title under it and a smaller title under that (the type scale, ui.css). */
|
|
221
|
+
.prose :where(h2) {
|
|
222
|
+
font-size: var(--type-headline-medium-size);
|
|
223
|
+
line-height: var(--type-headline-medium-line-height);
|
|
224
|
+
font-weight: var(--type-headline-medium-weight);
|
|
225
|
+
letter-spacing: var(--type-headline-medium-tracking);
|
|
226
|
+
}
|
|
227
|
+
.prose :where(h3) {
|
|
228
|
+
font-size: var(--type-title-large-size);
|
|
229
|
+
line-height: var(--type-title-large-line-height);
|
|
230
|
+
font-weight: var(--type-title-large-weight);
|
|
231
|
+
letter-spacing: var(--type-title-large-tracking);
|
|
232
|
+
}
|
|
233
|
+
.prose :where(h4) {
|
|
234
|
+
font-size: var(--type-title-medium-size);
|
|
235
|
+
line-height: var(--type-title-medium-line-height);
|
|
236
|
+
font-weight: var(--type-title-medium-weight);
|
|
237
|
+
letter-spacing: var(--type-title-medium-tracking);
|
|
238
|
+
}
|
|
239
|
+
/* Code in a line (cookie names in the privacy policy): on a tint, without the plugin's backticks. */
|
|
240
|
+
.prose :where(:not(pre) > code):not(:where(.not-prose code)) {
|
|
241
|
+
background: var(--tint);
|
|
242
|
+
border-radius: 0.3em;
|
|
243
|
+
padding: 0.1em 0.35em;
|
|
244
|
+
font-weight: 500;
|
|
245
|
+
}
|
|
246
|
+
.prose :where(:not(pre) > code):not(:where(.not-prose code))::before,
|
|
247
|
+
.prose :where(:not(pre) > code):not(:where(.not-prose code))::after {
|
|
248
|
+
content: none;
|
|
249
|
+
}
|
|
250
|
+
/* Not the links of a block in the text (not-prose): their look is the block's. */
|
|
251
|
+
.prose :where(a):not(:where(.not-prose a)) {
|
|
252
|
+
font-weight: 500;
|
|
253
|
+
text-decoration-thickness: 1px;
|
|
254
|
+
text-underline-offset: 0.2em;
|
|
255
|
+
}
|
|
256
|
+
/* Pictures in the text; not those in a block of the site in it (not-prose, RichText.astro). */
|
|
257
|
+
.prose :where(img):not(:where(.not-prose img)) {
|
|
258
|
+
border-radius: var(--radius);
|
|
259
|
+
width: 100%;
|
|
260
|
+
height: auto;
|
|
261
|
+
}
|
|
262
|
+
/* A picture in the text cut to a format in the editor (packages/editor/lib/crop.mjs): the box is the format, the
|
|
227
263
|
picture in it as large and as far moved as the part chosen. */
|
|
228
|
-
.picture-crop {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
.picture-crop > img {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
/* Video embeds (an embed that needs another size, an app's calendar, sets its own). */
|
|
244
|
-
.prose :where(iframe) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
264
|
+
.picture-crop {
|
|
265
|
+
display: block;
|
|
266
|
+
position: relative;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
border-radius: var(--radius);
|
|
269
|
+
margin-block: 2em;
|
|
270
|
+
}
|
|
271
|
+
.picture-crop > img {
|
|
272
|
+
position: absolute;
|
|
273
|
+
inset: 0 auto auto 0;
|
|
274
|
+
max-width: none;
|
|
275
|
+
height: auto;
|
|
276
|
+
margin: 0;
|
|
277
|
+
border-radius: 0;
|
|
278
|
+
}
|
|
279
|
+
/* Video embeds (an embed that needs another size, an app's calendar, sets its own). */
|
|
280
|
+
.prose :where(iframe) {
|
|
281
|
+
width: 100%;
|
|
282
|
+
aspect-ratio: 16 / 9;
|
|
283
|
+
height: auto;
|
|
284
|
+
border: 0;
|
|
285
|
+
border-radius: var(--radius);
|
|
286
|
+
margin-block: 2em;
|
|
287
|
+
}
|
|
252
288
|
|
|
253
|
-
/* Motion.
|
|
289
|
+
/* Motion.
|
|
254
290
|
[data-reveal]: sections fade up once when scrolled into view (packages/editor/theme/scripts/motion.ts).
|
|
255
291
|
The top of a page stands still from the first paint; from another page of the site it fades in (components.css).
|
|
256
292
|
Off for visitors who prefer reduced motion, and with the calm motion scheme ("Bevægelse", data-motion-scheme). */
|
|
257
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
294
|
+
.js:not([data-motion-scheme='calm']) [data-reveal] {
|
|
295
|
+
opacity: 0;
|
|
296
|
+
}
|
|
260
297
|
}
|
|
261
|
-
}
|
|
262
298
|
|
|
263
|
-
/* The site's logo (public/logo.svg), colored by background-color via a mask. */
|
|
264
|
-
.logo-mark {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
299
|
+
/* The site's logo (public/logo.svg), colored by background-color via a mask. */
|
|
300
|
+
.logo-mark {
|
|
301
|
+
display: inline-block;
|
|
302
|
+
-webkit-mask: url('/logo.svg') center / contain no-repeat;
|
|
303
|
+
mask: url('/logo.svg') center / contain no-repeat;
|
|
304
|
+
}
|
|
269
305
|
|
|
270
|
-
/* A block the theme places outside the text (Block.astro in EndBlock.astro and on the prices page): its
|
|
306
|
+
/* A block the theme places outside the text (Block.astro in EndBlock.astro and on the prices page): its
|
|
271
307
|
place gives the space around it, not the text's (.block-in-text). */
|
|
272
|
-
.block-place > .block-in-text {
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
/* The heading of a block placed outside the text (a front-page section): as the theme's other section headings
|
|
308
|
+
.block-place > .block-in-text {
|
|
309
|
+
margin-block: 0;
|
|
310
|
+
}
|
|
311
|
+
/* The heading of a block placed outside the text (a front-page section): as the theme's other section headings
|
|
276
312
|
(SectionHeading.tsx), not the text's; with a label or a subheading (.block-head, blocks.css) the space under the
|
|
277
313
|
whole head. */
|
|
278
|
-
.block-place > h2,
|
|
279
|
-
.block-place > .block-head {
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
.block-place > h2,
|
|
283
|
-
.block-place > .block-head > h2 {
|
|
284
|
-
font-size: var(--type-headline-large-size);
|
|
285
|
-
line-height: var(--type-headline-large-line-height);
|
|
286
|
-
font-weight: var(--type-headline-large-weight);
|
|
287
|
-
letter-spacing: var(--type-headline-large-tracking);
|
|
288
|
-
}
|
|
289
|
-
@media (width >= theme(--breakpoint-medium)) {
|
|
314
|
+
.block-place > h2,
|
|
315
|
+
.block-place > .block-head {
|
|
316
|
+
margin-block: 0 2rem;
|
|
317
|
+
}
|
|
290
318
|
.block-place > h2,
|
|
291
319
|
.block-place > .block-head > h2 {
|
|
292
|
-
font-size: var(--type-
|
|
293
|
-
line-height: var(--type-
|
|
294
|
-
font-weight: var(--type-
|
|
295
|
-
letter-spacing: var(--type-
|
|
320
|
+
font-size: var(--type-headline-large-size);
|
|
321
|
+
line-height: var(--type-headline-large-line-height);
|
|
322
|
+
font-weight: var(--type-headline-large-weight);
|
|
323
|
+
letter-spacing: var(--type-headline-large-tracking);
|
|
296
324
|
}
|
|
297
|
-
|
|
298
|
-
|
|
325
|
+
@media (width >= theme(--breakpoint-medium)) {
|
|
326
|
+
.block-place > h2,
|
|
327
|
+
.block-place > .block-head > h2 {
|
|
328
|
+
font-size: var(--type-display-medium-size);
|
|
329
|
+
line-height: var(--type-display-medium-line-height);
|
|
330
|
+
font-weight: var(--type-display-medium-weight);
|
|
331
|
+
letter-spacing: var(--type-display-medium-tracking);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/* Cards placed in the page's full width (a front-page section, PageBlock.astro): three in a row on a computer, as
|
|
299
335
|
there is room for (in the text, two). */
|
|
300
|
-
@media (width >= theme(--breakpoint-expanded)) {
|
|
301
|
-
|
|
302
|
-
|
|
336
|
+
@media (width >= theme(--breakpoint-expanded)) {
|
|
337
|
+
.block-place .block-cards {
|
|
338
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
339
|
+
}
|
|
303
340
|
}
|
|
304
|
-
}
|
|
305
341
|
|
|
306
|
-
/* The mark before the name on a testimonial (the standard block's card): a small dot in the accent, which the standard
|
|
342
|
+
/* The mark before the name on a testimonial (the standard block's card): a small dot in the accent, which the standard
|
|
307
343
|
theme's own look (global.css) makes an avatar with the initial. Another mark (e.g. a logo) is that
|
|
308
344
|
theme's own (its global.css), not the standard theme's. */
|
|
309
|
-
.block-testimonial-mark {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
345
|
+
.block-testimonial-mark {
|
|
346
|
+
display: inline-block;
|
|
347
|
+
flex-shrink: 0;
|
|
348
|
+
width: 0.5rem;
|
|
349
|
+
height: 0.5rem;
|
|
350
|
+
border-radius: 9999px;
|
|
351
|
+
background-color: var(--accent);
|
|
352
|
+
}
|
|
317
353
|
|
|
318
|
-
/* Boxes side by side ("Links", an app's boxes): the site's panels (outlined cards, ui.css, without a shadow), with
|
|
354
|
+
/* Boxes side by side ("Links", an app's boxes): the site's panels (outlined cards, ui.css, without a shadow), with
|
|
319
355
|
more room on a computer and the heading of a section in it. */
|
|
320
|
-
.block-panels .block-panel-title {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
356
|
+
.block-panels .block-panel-title {
|
|
357
|
+
font-size: var(--type-headline-small-size);
|
|
358
|
+
line-height: var(--type-headline-small-line-height);
|
|
359
|
+
font-weight: var(--type-headline-small-weight);
|
|
360
|
+
letter-spacing: var(--type-headline-small-tracking);
|
|
361
|
+
}
|
|
325
362
|
}
|
|
326
363
|
@media (width >= theme(--breakpoint-medium)) {
|
|
327
364
|
.block-panels > .block-panel {
|