ferst-core 0.1.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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +73 -0
- package/components/Button.astro +208 -0
- package/components/Card.astro +59 -0
- package/components/CookiePreferences.astro +232 -0
- package/components/DocLayout.astro +292 -0
- package/components/GroupCard.astro +164 -0
- package/components/LatestPostList.astro +64 -0
- package/components/PaginationNav.astro +81 -0
- package/components/PostCard.astro +262 -0
- package/components/PostsToolbar.astro +83 -0
- package/components/SecondaryLogo.astro +63 -0
- package/components/SiteFooter.astro +245 -0
- package/components/SiteHeader.astro +1130 -0
- package/content/collections.ts +40 -0
- package/content/filters.ts +107 -0
- package/content/schemas.ts +210 -0
- package/layouts/Base.astro +414 -0
- package/lib/mailLinks.ts +13 -0
- package/lib/mapsLink.ts +7 -0
- package/lib/siteSettings.ts +78 -0
- package/package.json +48 -0
- package/styles/fonts.css +76 -0
- package/styles/theme.css +90 -0
- package/utils/excerpt.ts +56 -0
- package/utils/formatDate.ts +22 -0
- package/utils/freshness.ts +5 -0
- package/utils/scrollReveal.ts +40 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import Card from './Card.astro';
|
|
4
|
+
import PostCard from './PostCard.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
posts: CollectionEntry<'posts'>[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { posts } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<ul class="latest-list">
|
|
14
|
+
{
|
|
15
|
+
posts.map((post) => (
|
|
16
|
+
<li class="latest-list__item">
|
|
17
|
+
<Card padding="0" class="latest-list__card">
|
|
18
|
+
<PostCard post={post} withImage />
|
|
19
|
+
</Card>
|
|
20
|
+
</li>
|
|
21
|
+
))
|
|
22
|
+
}
|
|
23
|
+
</ul>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.latest-list {
|
|
27
|
+
list-style: none;
|
|
28
|
+
padding: 0;
|
|
29
|
+
margin: 0;
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: 1fr;
|
|
32
|
+
gap: 1rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@media (min-width: 48em) {
|
|
36
|
+
.latest-list {
|
|
37
|
+
grid-template-columns: repeat(2, 1fr);
|
|
38
|
+
gap: 1.25rem;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (min-width: 64em) {
|
|
43
|
+
.latest-list {
|
|
44
|
+
grid-template-columns: repeat(3, 1fr);
|
|
45
|
+
gap: 1.5rem;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* display: grid (not flex/block) so this reliably stretches its one child
|
|
50
|
+
(.latest-list__card) to the full row height — a plain block wrapper
|
|
51
|
+
doesn't pass a grid-stretched height down to a nested child the same
|
|
52
|
+
way. Kept as a separate element from the Card itself so the
|
|
53
|
+
reveal-on-scroll transform/opacity (armReveal, applied in
|
|
54
|
+
[...page].astro) doesn't collide with Card's own hover transition on
|
|
55
|
+
the same element. */
|
|
56
|
+
.latest-list__item {
|
|
57
|
+
display: grid;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.latest-list__card :global(.post-card) {
|
|
61
|
+
border-bottom: none;
|
|
62
|
+
padding: 0;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Button from './Button.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
currentPage: number;
|
|
6
|
+
lastPage: number;
|
|
7
|
+
prevUrl: string | undefined;
|
|
8
|
+
nextUrl: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { currentPage, lastPage, prevUrl, nextUrl } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<nav class="pagination-nav" aria-label="Pagination navigation">
|
|
15
|
+
<div class="pagination-nav__controls">
|
|
16
|
+
{prevUrl ? (
|
|
17
|
+
<Button href={prevUrl} variant="primary" size="sm" rel="prev" class="pagination-nav__btn" style="border-radius: 8px;">
|
|
18
|
+
← Prev
|
|
19
|
+
</Button>
|
|
20
|
+
) : (
|
|
21
|
+
<Button variant="primary" size="sm" disabled aria-disabled="true" class="pagination-nav__btn" style="border-radius: 8px;">
|
|
22
|
+
← Prev
|
|
23
|
+
</Button>
|
|
24
|
+
)}
|
|
25
|
+
|
|
26
|
+
<span class="pagination-nav__info">
|
|
27
|
+
Page {currentPage} of {lastPage}
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
{nextUrl ? (
|
|
31
|
+
<Button href={nextUrl} variant="primary" size="sm" rel="next" class="pagination-nav__btn" style="border-radius: 8px;">
|
|
32
|
+
Next →
|
|
33
|
+
</Button>
|
|
34
|
+
) : (
|
|
35
|
+
<Button variant="primary" size="sm" disabled aria-disabled="true" class="pagination-nav__btn" style="border-radius: 8px;">
|
|
36
|
+
Next →
|
|
37
|
+
</Button>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
</nav>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.pagination-nav {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.pagination-nav__controls {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
gap: clamp(1.5rem, 3vw, 2.5rem);
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.pagination-nav__info {
|
|
57
|
+
font-family: 'Inter', sans-serif;
|
|
58
|
+
font-size: clamp(0.875rem, 1.5vw, 1rem);
|
|
59
|
+
line-height: 1.4;
|
|
60
|
+
color: var(--muted);
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:global([data-theme='dark']) .pagination-nav__info {
|
|
65
|
+
color: var(--text-light);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Mobile: tighter spacing */
|
|
69
|
+
@media (max-width: 47.9375em) {
|
|
70
|
+
.pagination-nav__controls {
|
|
71
|
+
gap: 0.75rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.pagination-nav__info {
|
|
75
|
+
font-size: 0.8rem;
|
|
76
|
+
order: 3;
|
|
77
|
+
width: 100%;
|
|
78
|
+
text-align: center;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import { opensAttachmentDirectly } from '../content/filters';
|
|
4
|
+
import { plainTextExcerpt } from '../utils/excerpt';
|
|
5
|
+
import { isFreshPost } from '../utils/freshness';
|
|
6
|
+
import { formatDate } from '../utils/formatDate';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
post: CollectionEntry<'posts'>;
|
|
10
|
+
withImage?: boolean;
|
|
11
|
+
showExcerpt?: boolean;
|
|
12
|
+
/** Whole-tile click (stretched-link on the title, no separate "Read" link) —
|
|
13
|
+
used by /posts/. */
|
|
14
|
+
linkWholeCard?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { post, withImage = false, showExcerpt = true, linkWholeCard = true } = Astro.props;
|
|
18
|
+
// A body-less post with an attachment (the typical newsletter shape) has
|
|
19
|
+
// nothing to show on its detail page, so the tile opens the attachment
|
|
20
|
+
// directly instead — same interaction as before the newsletters collection
|
|
21
|
+
// existed. Any post with a body still opens /posts/<slug>/ normally, even if
|
|
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) : '';
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<article class="post-card" class:list={{ 'post-card--with-image': withImage }}>
|
|
35
|
+
{isFresh && (
|
|
36
|
+
<span class="new-flag" data-fresh-since={post.data.date.toISOString()}>New</span>
|
|
37
|
+
)}
|
|
38
|
+
{withImage && (
|
|
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
|
+
)
|
|
50
|
+
)}
|
|
51
|
+
<div class="post-content">
|
|
52
|
+
{displayTags.length > 0 && (
|
|
53
|
+
<div class="post-tags" aria-label="Post tags">
|
|
54
|
+
{displayTags.map((tag) => (
|
|
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
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</article>
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
.post-card {
|
|
83
|
+
position: relative;
|
|
84
|
+
padding: 0;
|
|
85
|
+
border-bottom: none;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
flex: 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.post-content {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
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
|
+
}
|
|
101
|
+
|
|
102
|
+
.post-tags {
|
|
103
|
+
display: flex;
|
|
104
|
+
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
|
+
align-items: center;
|
|
114
|
+
font-family: 'Poppins', sans-serif;
|
|
115
|
+
font-size: 0.65rem;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
text-transform: uppercase;
|
|
118
|
+
letter-spacing: 0.12em;
|
|
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;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Stretched-link pattern: the title anchor's ::after is expanded to cover
|
|
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;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.post-card h3 a::after {
|
|
147
|
+
content: '';
|
|
148
|
+
position: absolute;
|
|
149
|
+
inset: 0;
|
|
150
|
+
z-index: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.new-flag {
|
|
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;
|
|
175
|
+
}
|
|
176
|
+
:global(html[data-theme="dark"]) .new-flag {
|
|
177
|
+
background: var(--gold);
|
|
178
|
+
color: #2A1F12;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.post-content > p {
|
|
182
|
+
font-size: clamp(0.8125rem, 0.7rem + 0.5vw, 0.9375rem);
|
|
183
|
+
line-height: 1.55;
|
|
184
|
+
display: -webkit-box;
|
|
185
|
+
-webkit-line-clamp: 3;
|
|
186
|
+
-webkit-box-orient: vertical;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
}
|
|
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;
|
|
198
|
+
margin-top: auto;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.post-card__footer time {
|
|
202
|
+
flex-shrink: 0;
|
|
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);
|
|
222
|
+
color: var(--gold);
|
|
223
|
+
}
|
|
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
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
availableTags: string[];
|
|
4
|
+
activeTag?: string;
|
|
5
|
+
tagCounts: Record<string, number>;
|
|
6
|
+
totalCount: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { availableTags, activeTag = '', tagCounts, totalCount } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="tag-filters" role="group" aria-label="Filter posts by category">
|
|
13
|
+
<a
|
|
14
|
+
href="/posts/"
|
|
15
|
+
class:list={['tag-filter', { active: !activeTag }]}
|
|
16
|
+
aria-current={!activeTag ? 'true' : undefined}
|
|
17
|
+
>
|
|
18
|
+
All Posts ({totalCount})
|
|
19
|
+
</a>
|
|
20
|
+
|
|
21
|
+
{availableTags.map((tag) => (
|
|
22
|
+
<a
|
|
23
|
+
href={`/posts/tag/${tag}/`}
|
|
24
|
+
class:list={['tag-filter', { active: activeTag === tag }]}
|
|
25
|
+
aria-current={activeTag === tag ? 'true' : undefined}
|
|
26
|
+
>
|
|
27
|
+
{tag} ({tagCounts[tag] ?? 0})
|
|
28
|
+
</a>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.tag-filters {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: 0.55rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.tag-filter {
|
|
40
|
+
font-family: 'Poppins', sans-serif;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
font-size: 0.75rem;
|
|
43
|
+
letter-spacing: 0.03em;
|
|
44
|
+
padding: 0.5rem 1rem;
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
background: rgba(200, 151, 58, 0.1);
|
|
47
|
+
color: var(--fg);
|
|
48
|
+
border: 1px solid transparent;
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
text-transform: capitalize;
|
|
51
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tag-filter:hover {
|
|
55
|
+
border-color: var(--gold);
|
|
56
|
+
color: var(--gold);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tag-filter:focus-visible {
|
|
60
|
+
outline: 2px solid var(--gold);
|
|
61
|
+
outline-offset: 2px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tag-filter.active {
|
|
65
|
+
background: var(--gold);
|
|
66
|
+
color: #2A1F12;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:global([data-theme='dark']) .tag-filter {
|
|
70
|
+
background: rgba(255, 255, 255, 0.06);
|
|
71
|
+
color: var(--text-light);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:global([data-theme='dark']) .tag-filter:hover {
|
|
75
|
+
border-color: var(--gold);
|
|
76
|
+
color: var(--gold);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:global([data-theme='dark']) .tag-filter.active {
|
|
80
|
+
background: var(--gold);
|
|
81
|
+
color: #2A1F12;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Secondary/footer logo: theme-aware variants and the link it wraps come
|
|
3
|
+
* from CMS-backed `siteSettings` and are passed in from `Base.astro`. When
|
|
4
|
+
* missing the component falls back to the local `public/uploads` assets so
|
|
5
|
+
* legacy JSON still renders a logo.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
logoLight?: string;
|
|
9
|
+
logoDark?: string;
|
|
10
|
+
link?: { name: string; url: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { logoLight: propLogoLight, logoDark: propLogoDark, link } = Astro.props as Props;
|
|
14
|
+
const linkName = link?.name ?? '';
|
|
15
|
+
const linkUrl = link?.url ?? '';
|
|
16
|
+
const label = linkName ? `${linkName} (opens in new tab)` : '';
|
|
17
|
+
|
|
18
|
+
const srcLight = propLogoLight ?? propLogoDark ?? '';
|
|
19
|
+
const srcDark = propLogoDark ?? propLogoLight ?? '';
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class="secondary-logo">
|
|
23
|
+
<a href={linkUrl} target="_blank" rel="noopener noreferrer" aria-label={label}>
|
|
24
|
+
<img
|
|
25
|
+
id="secondary-logo-img"
|
|
26
|
+
class="secondary-logo__img"
|
|
27
|
+
src={srcLight}
|
|
28
|
+
data-src-light={srcLight}
|
|
29
|
+
data-src-dark={srcDark}
|
|
30
|
+
alt=""
|
|
31
|
+
width="150"
|
|
32
|
+
height="82"
|
|
33
|
+
loading="lazy"
|
|
34
|
+
decoding="async"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
/>
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script is:inline define:vars={{ srcLight, srcDark }}>
|
|
41
|
+
(function () {
|
|
42
|
+
var html = document.documentElement;
|
|
43
|
+
var img = document.getElementById('secondary-logo-img');
|
|
44
|
+
if (!img) return;
|
|
45
|
+
|
|
46
|
+
function sync() {
|
|
47
|
+
var t = html.dataset.theme === 'dark' ? 'dark' : 'light';
|
|
48
|
+
var dataLight = img.getAttribute('data-src-light');
|
|
49
|
+
var dataDark = img.getAttribute('data-src-dark');
|
|
50
|
+
if (t === 'dark') {
|
|
51
|
+
img.src = dataDark || dataLight || srcDark;
|
|
52
|
+
} else {
|
|
53
|
+
img.src = dataLight || dataDark || srcLight;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sync();
|
|
58
|
+
var colorSchemeMq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
59
|
+
colorSchemeMq.addEventListener('change', sync);
|
|
60
|
+
var observer = new MutationObserver(function () { sync(); });
|
|
61
|
+
observer.observe(html, { attributes: true, attributeFilter: ['data-theme'] });
|
|
62
|
+
})();
|
|
63
|
+
</script>
|