ferst-core 0.1.0 → 0.2.1
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/LICENSE +102 -201
- package/LICENSE-Apache-2.0 +201 -0
- package/NOTICE +24 -5
- package/README.md +89 -73
- package/bin/check-thin.mjs +74 -0
- package/components/BlockRenderer.astro +89 -0
- package/components/Button.astro +85 -33
- package/components/CalendarEmbed.astro +188 -0
- package/components/Card.astro +7 -17
- package/components/Icon.astro +35 -0
- package/components/PostArticle.astro +133 -0
- package/components/PostCard.astro +93 -225
- package/components/SiteFooter.astro +2 -2
- package/components/SiteHeader.astro +104 -51
- package/components/blocks/Badge.astro +29 -0
- package/components/blocks/ButtonBlock.astro +13 -0
- package/components/blocks/Divider.astro +12 -0
- package/components/blocks/Grid.astro +48 -0
- package/components/blocks/Heading.astro +42 -0
- package/components/blocks/ImageBlock.astro +80 -0
- package/components/blocks/List.astro +78 -0
- package/components/blocks/Prose.astro +29 -0
- package/components/blocks/Quote.astro +32 -0
- package/components/blocks/Section.astro +52 -0
- package/components/blocks/Spacer.astro +20 -0
- package/components/blocks/Stack.astro +30 -0
- package/components/blocks/Stat.astro +30 -0
- package/components/recipes/Accordion.astro +84 -0
- package/components/recipes/Announcement.astro +74 -0
- package/components/recipes/Banner.astro +134 -0
- package/components/recipes/Bento.astro +138 -0
- package/components/recipes/ContactForm.astro +172 -0
- package/components/recipes/Cta.astro +83 -0
- package/components/recipes/Faq.astro +80 -0
- package/components/recipes/FeatureCards.astro +105 -0
- package/components/recipes/Gallery.astro +55 -0
- package/components/recipes/Hero.astro +191 -0
- package/components/recipes/Logos.astro +77 -0
- package/components/recipes/Marquee.astro +75 -0
- package/components/recipes/Pricing.astro +149 -0
- package/components/recipes/Stats.astro +124 -0
- package/components/recipes/Steps.astro +120 -0
- package/components/recipes/Tabs.astro +133 -0
- package/components/recipes/Testimonial.astro +66 -0
- package/components/recipes/Tiles.astro +248 -0
- package/content/blocks.ts +534 -0
- package/content/calendar.ts +40 -0
- package/content/collections.ts +25 -14
- package/content/posts.ts +89 -0
- package/content/schemas.ts +58 -123
- package/layouts/Base.astro +30 -56
- package/lib/calendar.ts +12 -0
- package/lib/icons.ts +64 -0
- package/lib/pages.ts +42 -0
- package/lib/posts.ts +93 -0
- package/lib/siteSettings.ts +18 -36
- package/lib/themeTokens.ts +163 -0
- package/package.json +61 -48
- package/styles/theme.css +42 -0
- package/components/DocLayout.astro +0 -292
- package/components/GroupCard.astro +0 -164
- package/components/LatestPostList.astro +0 -64
- package/components/PaginationNav.astro +0 -81
- package/components/PostsToolbar.astro +0 -83
- package/content/filters.ts +0 -107
- package/lib/mailLinks.ts +0 -13
- package/lib/mapsLink.ts +0 -7
- package/utils/excerpt.ts +0 -56
- package/utils/formatDate.ts +0 -22
- package/utils/freshness.ts +0 -5
|
@@ -1,262 +1,130 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* PostCard — a post preview for the news index and tag pages. Token-driven (radius,
|
|
4
|
+
* border, shadow, colours all from the theme), equal-height in a grid, with the
|
|
5
|
+
* whole card clickable via a stretched title link. White-room (no CTK code).
|
|
6
|
+
*/
|
|
8
7
|
interface Props {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
href: string;
|
|
9
|
+
title: string;
|
|
10
|
+
date: Date | string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
heroImage?: string;
|
|
13
|
+
/** Optional tag labels shown as a small eyebrow. */
|
|
14
|
+
tagLabels?: string[];
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// it also carries an attachment (offered there as a separate link).
|
|
23
|
-
const directAttachment = opensAttachmentDirectly(post);
|
|
24
|
-
const href = directAttachment ? post.data.attachment! : `/posts/${post.slug}`;
|
|
25
|
-
const linkTarget = directAttachment ? '_blank' : undefined;
|
|
26
|
-
const linkRel = directAttachment ? 'noopener noreferrer' : undefined;
|
|
27
|
-
const displayTags = post.data.tags;
|
|
28
|
-
const isFresh = isFreshPost(post.data.date);
|
|
29
|
-
const dateLabel = formatDate(post.data.date);
|
|
30
|
-
const hasImage = Boolean(post.data.image);
|
|
31
|
-
const excerpt = showExcerpt ? plainTextExcerpt(post.body ?? '', withImage ? 400 : 280) : '';
|
|
16
|
+
const { href, title, date, summary, heroImage, tagLabels = [] } = Astro.props;
|
|
17
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
18
|
+
const iso = Number.isNaN(d.valueOf()) ? undefined : d.toISOString().slice(0, 10);
|
|
19
|
+
const display = Number.isNaN(d.valueOf())
|
|
20
|
+
? ''
|
|
21
|
+
: d.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
|
32
22
|
---
|
|
33
23
|
|
|
34
|
-
<article class="post-card"
|
|
35
|
-
{
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
hasImage ? (
|
|
40
|
-
<img
|
|
41
|
-
src={post.data.image}
|
|
42
|
-
alt={post.data.title}
|
|
43
|
-
class="post-card__img"
|
|
44
|
-
loading="lazy"
|
|
45
|
-
decoding="async"
|
|
46
|
-
/>
|
|
47
|
-
) : (
|
|
48
|
-
<div class="post-card__placeholder" aria-hidden="true"></div>
|
|
49
|
-
)
|
|
24
|
+
<article class="b-post-card">
|
|
25
|
+
{heroImage && (
|
|
26
|
+
<a class="b-post-card__media" href={href} tabindex="-1" aria-hidden="true">
|
|
27
|
+
<img src={heroImage} alt="" loading="lazy" />
|
|
28
|
+
</a>
|
|
50
29
|
)}
|
|
51
|
-
<div class="post-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
<span class="post-tag">{tag}</span>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
<h3>{linkWholeCard ? <a href={href} target={linkTarget} rel={linkRel}>{post.data.title}</a> : post.data.title}</h3>
|
|
61
|
-
|
|
62
|
-
{excerpt ? <p>{excerpt}</p> : null}
|
|
63
|
-
|
|
64
|
-
<div class="post-card__footer">
|
|
65
|
-
<time datetime={post.data.date.toISOString()}>{dateLabel}</time>
|
|
66
|
-
{linkWholeCard ? (
|
|
67
|
-
<svg class="post-card__chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
68
|
-
<polyline points="9 6 15 12 9 18"/>
|
|
69
|
-
</svg>
|
|
70
|
-
) : (
|
|
71
|
-
<a href={href} target={linkTarget} rel={linkRel} aria-label={`Read ${post.data.title}`}>
|
|
72
|
-
<svg class="post-card__chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
73
|
-
<polyline points="9 6 15 12 9 18"/>
|
|
74
|
-
</svg>
|
|
75
|
-
</a>
|
|
76
|
-
)}
|
|
30
|
+
<div class="b-post-card__body">
|
|
31
|
+
<div class="b-post-card__meta">
|
|
32
|
+
{tagLabels.length > 0 && <span class="b-post-card__eyebrow">{tagLabels[0]}</span>}
|
|
33
|
+
{display && <time datetime={iso}>{display}</time>}
|
|
77
34
|
</div>
|
|
35
|
+
<h3 class="b-post-card__title">
|
|
36
|
+
<a href={href}>{title}</a>
|
|
37
|
+
</h3>
|
|
38
|
+
{summary && <p class="b-post-card__summary">{summary}</p>}
|
|
39
|
+
<span class="b-post-card__more" aria-hidden="true">Read more</span>
|
|
78
40
|
</div>
|
|
79
41
|
</article>
|
|
80
42
|
|
|
81
43
|
<style>
|
|
82
|
-
.post-card {
|
|
83
|
-
position: relative;
|
|
84
|
-
padding: 0;
|
|
85
|
-
border-bottom: none;
|
|
86
|
-
overflow: hidden;
|
|
44
|
+
.b-post-card {
|
|
87
45
|
display: flex;
|
|
88
46
|
flex-direction: column;
|
|
89
|
-
|
|
47
|
+
background: var(--bg-surface);
|
|
48
|
+
border: var(--card-border);
|
|
49
|
+
border-radius: var(--radius);
|
|
50
|
+
box-shadow: var(--shadow-sm);
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
position: relative;
|
|
53
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
|
90
54
|
}
|
|
91
|
-
|
|
92
|
-
|
|
55
|
+
.b-post-card:hover {
|
|
56
|
+
transform: translateY(-2px);
|
|
57
|
+
box-shadow: var(--shadow-md);
|
|
58
|
+
border-color: var(--gold);
|
|
59
|
+
}
|
|
60
|
+
.b-post-card__media {
|
|
61
|
+
display: block;
|
|
62
|
+
position: relative;
|
|
63
|
+
aspect-ratio: 16 / 9;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
background: var(--bg-section);
|
|
66
|
+
}
|
|
67
|
+
/* Absolutely-positioned + object-fit:cover so ANY source ratio is cropped to a
|
|
68
|
+
uniform 16:9 tile — never stretched, never overflowing into the text. The
|
|
69
|
+
compound selector (and inset/size) also out-specifies the global
|
|
70
|
+
`img { height:auto }` in Base, which otherwise lets tall images blow the box. */
|
|
71
|
+
.b-post-card .b-post-card__media img {
|
|
72
|
+
position: absolute;
|
|
73
|
+
inset: 0;
|
|
74
|
+
width: 100%;
|
|
75
|
+
height: 100%;
|
|
76
|
+
object-fit: cover;
|
|
77
|
+
display: block;
|
|
78
|
+
}
|
|
79
|
+
.b-post-card__body {
|
|
93
80
|
display: flex;
|
|
94
81
|
flex-direction: column;
|
|
82
|
+
gap: 0.5rem;
|
|
83
|
+
padding: 1.1rem 1.2rem 1.3rem;
|
|
95
84
|
flex: 1;
|
|
96
|
-
min-height: 0;
|
|
97
|
-
gap: 0.75rem;
|
|
98
|
-
padding: clamp(0.875rem, 2.2vw, 1.25rem);
|
|
99
|
-
color: var(--news-card-body);
|
|
100
85
|
}
|
|
101
|
-
|
|
102
|
-
.post-tags {
|
|
86
|
+
.b-post-card__meta {
|
|
103
87
|
display: flex;
|
|
104
88
|
flex-wrap: wrap;
|
|
105
|
-
justify-content: flex-end;
|
|
106
|
-
gap: 0.35rem;
|
|
107
|
-
margin: 0;
|
|
108
|
-
width: 100%;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.post-tag {
|
|
112
|
-
display: inline-flex;
|
|
113
89
|
align-items: center;
|
|
114
|
-
|
|
115
|
-
|
|
90
|
+
gap: 0.5rem 0.75rem;
|
|
91
|
+
}
|
|
92
|
+
.b-post-card__eyebrow {
|
|
93
|
+
font-family: var(--font-heading);
|
|
94
|
+
font-size: 0.7rem;
|
|
116
95
|
font-weight: 600;
|
|
96
|
+
letter-spacing: 0.08em;
|
|
117
97
|
text-transform: uppercase;
|
|
118
|
-
|
|
119
|
-
color: var(--brown);
|
|
120
|
-
background: rgba(180, 145, 70, 0.22);
|
|
121
|
-
border-radius: 4px;
|
|
122
|
-
padding: 0.15rem 0.45rem;
|
|
123
|
-
line-height: 1.2;
|
|
124
|
-
}
|
|
125
|
-
:global(html[data-theme="dark"]) .post-tag {
|
|
126
|
-
color: var(--text-light);
|
|
127
|
-
background: rgba(255, 255, 255, 0.12);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.post-content h3 {
|
|
131
|
-
/* Full title, never truncated — the admin's exact wording is shown
|
|
132
|
-
in full, however many lines it takes. */
|
|
133
|
-
margin-block: 0;
|
|
98
|
+
color: var(--gold);
|
|
134
99
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
the whole card (positioned against .post-card, the nearest positioned
|
|
138
|
-
ancestor), so clicking anywhere on the tile opens the post — no JS
|
|
139
|
-
needed. The footer "Read" link sits above it via z-index so it stays
|
|
140
|
-
independently clickable/focusable. */
|
|
141
|
-
.post-card h3 a {
|
|
142
|
-
color: inherit;
|
|
143
|
-
text-decoration: none;
|
|
100
|
+
.b-post-card__title {
|
|
101
|
+
margin: 0;
|
|
144
102
|
}
|
|
145
|
-
|
|
146
|
-
|
|
103
|
+
/* Stretch the title link over the whole card so the card is clickable, while
|
|
104
|
+
the meta/summary stay selectable. */
|
|
105
|
+
.b-post-card__title a::after {
|
|
147
106
|
content: '';
|
|
148
107
|
position: absolute;
|
|
149
108
|
inset: 0;
|
|
150
|
-
z-index: 1;
|
|
151
109
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
position: absolute;
|
|
155
|
-
top: clamp(0.5rem, 1.6vw, 0.75rem);
|
|
156
|
-
left: clamp(0.5rem, 1.6vw, 0.75rem);
|
|
157
|
-
z-index: 2;
|
|
158
|
-
display: inline-block;
|
|
159
|
-
font-family: 'Poppins', sans-serif;
|
|
160
|
-
font-size: 0.6875rem;
|
|
161
|
-
font-weight: 700;
|
|
162
|
-
letter-spacing: 0.12em;
|
|
163
|
-
text-transform: uppercase;
|
|
164
|
-
color: #fff;
|
|
165
|
-
background: var(--accent);
|
|
166
|
-
border-radius: 4px;
|
|
167
|
-
padding: 0.22rem 0.55rem;
|
|
168
|
-
line-height: 1.2;
|
|
169
|
-
transform: rotate(-4deg);
|
|
170
|
-
transform-origin: top left;
|
|
171
|
-
box-shadow:
|
|
172
|
-
0 2px 6px rgba(0, 0, 0, 0.18),
|
|
173
|
-
0 1px 2px rgba(0, 0, 0, 0.08);
|
|
174
|
-
pointer-events: none;
|
|
110
|
+
.b-post-card__title a {
|
|
111
|
+
color: var(--fg);
|
|
175
112
|
}
|
|
176
|
-
:
|
|
177
|
-
|
|
178
|
-
color: #2A1F12;
|
|
113
|
+
.b-post-card:hover .b-post-card__title a {
|
|
114
|
+
color: var(--gold);
|
|
179
115
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
116
|
+
.b-post-card__summary {
|
|
117
|
+
margin: 0;
|
|
118
|
+
color: var(--muted);
|
|
119
|
+
font-size: 0.95rem;
|
|
183
120
|
line-height: 1.55;
|
|
184
|
-
display: -webkit-box;
|
|
185
|
-
-webkit-line-clamp: 3;
|
|
186
|
-
-webkit-box-orient: vertical;
|
|
187
|
-
overflow: hidden;
|
|
188
121
|
}
|
|
189
|
-
|
|
190
|
-
/* Date on the left, chevron on the right — chevron keeps the same
|
|
191
|
-
slide-right-on-hover language as the homepage's Latest from the
|
|
192
|
-
Notice rows (.notice-row / .notice-chevron). */
|
|
193
|
-
.post-card__footer {
|
|
194
|
-
display: flex;
|
|
195
|
-
justify-content: space-between;
|
|
196
|
-
align-items: center;
|
|
197
|
-
gap: 0.4rem;
|
|
122
|
+
.b-post-card__more {
|
|
198
123
|
margin-top: auto;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.post-card__footer a {
|
|
206
|
-
position: relative;
|
|
207
|
-
z-index: 2;
|
|
208
|
-
display: inline-flex;
|
|
209
|
-
align-items: center;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.post-card__chevron {
|
|
213
|
-
flex-shrink: 0;
|
|
214
|
-
color: var(--muted);
|
|
215
|
-
transition: transform 0.15s ease, color 0.15s ease;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/* Decorative in linkWholeCard mode (the real link is the title's
|
|
219
|
-
stretched-link), so it reacts to hover on the whole card. */
|
|
220
|
-
.post-card:hover .post-card__chevron {
|
|
221
|
-
transform: translateX(3px);
|
|
124
|
+
padding-top: 0.4rem;
|
|
125
|
+
font-family: var(--font-heading);
|
|
126
|
+
font-size: 0.85rem;
|
|
127
|
+
font-weight: 600;
|
|
222
128
|
color: var(--gold);
|
|
223
129
|
}
|
|
224
|
-
|
|
225
|
-
/* With-image variant: opt-in via the `withImage` prop, used by the
|
|
226
|
-
/posts/ listing. Image always sits above the text, at every screen
|
|
227
|
-
size. */
|
|
228
|
-
.post-card--with-image .post-card__img,
|
|
229
|
-
.post-card--with-image .post-card__placeholder {
|
|
230
|
-
display: block;
|
|
231
|
-
width: 100%;
|
|
232
|
-
aspect-ratio: 16 / 9;
|
|
233
|
-
border-radius: 10px 10px 0 0;
|
|
234
|
-
margin: 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.post-card--with-image .post-card__img {
|
|
238
|
-
object-fit: cover;
|
|
239
|
-
object-position: center;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.post-card--with-image .post-card__placeholder {
|
|
243
|
-
background: linear-gradient(135deg, var(--gold-light) 0%, var(--gold) 100%);
|
|
244
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
:global(html[data-theme="dark"]) .post-card--with-image .post-card__placeholder {
|
|
248
|
-
background: linear-gradient(135deg, #4A6741 0%, #2C3B2A 100%);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.post-card--with-image .post-content > p {
|
|
252
|
-
-webkit-line-clamp: 5;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.post-card--with-image .post-tags {
|
|
256
|
-
justify-content: flex-start;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
.post-card--with-image .post-content {
|
|
260
|
-
padding-top: clamp(1.5rem, 3.5vw, 2rem);
|
|
261
|
-
}
|
|
262
130
|
</style>
|
|
@@ -131,7 +131,7 @@ const year = new Date().getFullYear();
|
|
|
131
131
|
}
|
|
132
132
|
.site-footer .footer-brand {
|
|
133
133
|
margin: 0;
|
|
134
|
-
font-family:
|
|
134
|
+
font-family: var(--font-heading);
|
|
135
135
|
font-weight: 700;
|
|
136
136
|
font-size: 1.15rem;
|
|
137
137
|
line-height: 1.3;
|
|
@@ -217,7 +217,7 @@ const year = new Date().getFullYear();
|
|
|
217
217
|
}
|
|
218
218
|
.site-footer .footer-copy {
|
|
219
219
|
margin: 0;
|
|
220
|
-
font-family:
|
|
220
|
+
font-family: var(--font-body);
|
|
221
221
|
font-size: 0.68rem;
|
|
222
222
|
font-weight: 400;
|
|
223
223
|
color: var(--muted);
|