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
|
@@ -15,8 +15,8 @@ import type { NavLink, NavGroup } from '../content/schemas';
|
|
|
15
15
|
interface Props {
|
|
16
16
|
path: string;
|
|
17
17
|
headerClass?: string;
|
|
18
|
-
primaryLogoLight
|
|
19
|
-
primaryLogoDark
|
|
18
|
+
primaryLogoLight?: string;
|
|
19
|
+
primaryLogoDark?: string;
|
|
20
20
|
brandAlt: string;
|
|
21
21
|
primaryNav: NavLink[];
|
|
22
22
|
/** Optional dropdown / mega-menu group. Client-configured — the core ships none. */
|
|
@@ -56,28 +56,34 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
56
56
|
-->
|
|
57
57
|
<header class:list={['site-header', headerClass]} transition:persist transition:name="site-header" transition:animate="none">
|
|
58
58
|
<a href="/" class="brand">
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
{primaryLogoLight ? (
|
|
60
|
+
<img
|
|
61
|
+
id="primary-logo"
|
|
62
|
+
src={primaryLogoLight}
|
|
63
|
+
data-src-light={primaryLogoLight}
|
|
64
|
+
data-src-dark={primaryLogoDark ?? primaryLogoLight}
|
|
65
|
+
alt={brandAlt}
|
|
66
|
+
class="primary-logo"
|
|
67
|
+
width="545"
|
|
68
|
+
height="151"
|
|
69
|
+
loading="eager"
|
|
70
|
+
fetchpriority="high"
|
|
71
|
+
decoding="sync"
|
|
72
|
+
/>
|
|
73
|
+
) : (
|
|
74
|
+
<span class="brand-wordmark">{brandAlt}</span>
|
|
75
|
+
)}
|
|
72
76
|
</a>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
{primaryLogoLight && (
|
|
78
|
+
<script is:inline define:vars={{ primaryLogoDark, primaryLogoLight }}>
|
|
79
|
+
(function () {
|
|
80
|
+
var img = document.getElementById('primary-logo');
|
|
81
|
+
if (!img) return;
|
|
82
|
+
var t = document.documentElement.dataset.theme;
|
|
83
|
+
img.src = t === 'dark' ? (primaryLogoDark || primaryLogoLight) : primaryLogoLight;
|
|
84
|
+
})();
|
|
85
|
+
</script>
|
|
86
|
+
)}
|
|
81
87
|
|
|
82
88
|
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
|
|
83
89
|
{dropdown && (
|
|
@@ -196,6 +202,21 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
196
202
|
width: auto;
|
|
197
203
|
}
|
|
198
204
|
|
|
205
|
+
/* Text wordmark fallback — shown when a client has configured no logo image
|
|
206
|
+
(var(--fg) auto-switches for dark mode via theme.css). */
|
|
207
|
+
.brand-wordmark {
|
|
208
|
+
font-family: var(--font-heading);
|
|
209
|
+
font-weight: 700;
|
|
210
|
+
font-size: 1.25rem;
|
|
211
|
+
line-height: 1;
|
|
212
|
+
color: var(--fg);
|
|
213
|
+
letter-spacing: 0.01em;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
}
|
|
216
|
+
@media (min-width: 64em) {
|
|
217
|
+
.brand-wordmark { font-size: 1.5rem; }
|
|
218
|
+
}
|
|
219
|
+
|
|
199
220
|
/* ── Header ── */
|
|
200
221
|
/* Logo + header: mobile-first — compact until desktop ≥64em scales up (matches button/theme sizing). */
|
|
201
222
|
.site-header {
|
|
@@ -218,6 +239,31 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
218
239
|
}
|
|
219
240
|
}
|
|
220
241
|
[data-theme="dark"] .site-header { background: var(--dark-bg); }
|
|
242
|
+
|
|
243
|
+
/* Sticky "shrink on scroll": the header condenses and gains a shadow once the
|
|
244
|
+
page is scrolled, then expands again at the top. Purely additive; safe with
|
|
245
|
+
the sticky positioning. Motion honoured. */
|
|
246
|
+
.site-header {
|
|
247
|
+
transition: height 0.25s ease, box-shadow 0.25s ease;
|
|
248
|
+
}
|
|
249
|
+
.site-header.is-compact {
|
|
250
|
+
box-shadow: 0 2px 14px rgba(15, 23, 42, 0.10);
|
|
251
|
+
height: 50px;
|
|
252
|
+
}
|
|
253
|
+
@media (min-width: 64em) {
|
|
254
|
+
.site-header.is-compact { height: 54px; }
|
|
255
|
+
}
|
|
256
|
+
.site-header .primary-logo,
|
|
257
|
+
.site-header .brand-wordmark {
|
|
258
|
+
transition: height 0.25s ease, font-size 0.25s ease;
|
|
259
|
+
}
|
|
260
|
+
.site-header.is-compact .primary-logo { height: 38px; }
|
|
261
|
+
.site-header.is-compact .brand-wordmark { font-size: 1.15rem; }
|
|
262
|
+
@media (prefers-reduced-motion: reduce) {
|
|
263
|
+
.site-header,
|
|
264
|
+
.site-header .primary-logo,
|
|
265
|
+
.site-header .brand-wordmark { transition: none; }
|
|
266
|
+
}
|
|
221
267
|
.brand {
|
|
222
268
|
text-decoration: none;
|
|
223
269
|
display: flex;
|
|
@@ -265,7 +311,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
265
311
|
padding: 14px 0;
|
|
266
312
|
color: var(--accent);
|
|
267
313
|
text-decoration: none;
|
|
268
|
-
font-family:
|
|
314
|
+
font-family: var(--font-body);
|
|
269
315
|
font-size: 0.875rem;
|
|
270
316
|
font-weight: 500;
|
|
271
317
|
letter-spacing: 0.1em;
|
|
@@ -316,10 +362,10 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
316
362
|
white-space: nowrap;
|
|
317
363
|
}
|
|
318
364
|
.nav-cta:hover,
|
|
319
|
-
.nav-cta.active { background: #
|
|
320
|
-
[data-theme="dark"] .nav-cta { background: var(--gold); color: var(--
|
|
365
|
+
.nav-cta.active { background: color-mix(in srgb, var(--gold) 85%, #000); color: var(--text-light); border: none; }
|
|
366
|
+
[data-theme="dark"] .nav-cta { background: var(--gold); color: var(--dark-bg); border: none; }
|
|
321
367
|
[data-theme="dark"] .nav-cta:hover,
|
|
322
|
-
[data-theme="dark"] .nav-cta.active { background: var(--
|
|
368
|
+
[data-theme="dark"] .nav-cta.active { background: color-mix(in srgb, var(--gold) 85%, #fff); color: var(--dark-bg); border: none; }
|
|
323
369
|
|
|
324
370
|
.nav-mobile-theme .nav-cta--drawer {
|
|
325
371
|
display: none;
|
|
@@ -389,7 +435,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
389
435
|
}
|
|
390
436
|
.theme-icon--sun { opacity: 1; }
|
|
391
437
|
[data-theme="dark"] .theme-icon--sun { opacity: 0.5; }
|
|
392
|
-
[data-theme="dark"] .theme-icon--moon { opacity: 1; color:
|
|
438
|
+
[data-theme="dark"] .theme-icon--moon { opacity: 1; color: var(--dark-bg); }
|
|
393
439
|
|
|
394
440
|
/* Mobile theme toggle row — hidden on desktop */
|
|
395
441
|
.nav-mobile-theme { display: none; }
|
|
@@ -416,7 +462,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
416
462
|
position: absolute;
|
|
417
463
|
width: 22px;
|
|
418
464
|
height: 2px;
|
|
419
|
-
background:
|
|
465
|
+
background: var(--fg);
|
|
420
466
|
border-radius: 2px;
|
|
421
467
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease, top 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
422
468
|
transform-origin: center;
|
|
@@ -501,7 +547,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
501
547
|
margin: 0;
|
|
502
548
|
font-size: 1rem;
|
|
503
549
|
color: var(--accent);
|
|
504
|
-
font-family:
|
|
550
|
+
font-family: var(--font-heading);
|
|
505
551
|
font-weight: 600;
|
|
506
552
|
letter-spacing: 0.04em;
|
|
507
553
|
}
|
|
@@ -567,7 +613,7 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
567
613
|
font-size: 0.875rem;
|
|
568
614
|
font-weight: 600;
|
|
569
615
|
color: var(--accent);
|
|
570
|
-
font-family:
|
|
616
|
+
font-family: var(--font-body);
|
|
571
617
|
line-height: 1.3;
|
|
572
618
|
transition: color 0.15s;
|
|
573
619
|
}
|
|
@@ -576,14 +622,14 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
576
622
|
font-size: 0.75rem;
|
|
577
623
|
color: var(--accent);
|
|
578
624
|
font-weight: 500;
|
|
579
|
-
font-family:
|
|
625
|
+
font-family: var(--font-body);
|
|
580
626
|
line-height: 1.4;
|
|
581
627
|
}
|
|
582
628
|
.mega-about-sub {
|
|
583
629
|
font-size: 0.8rem;
|
|
584
630
|
color: var(--accent);
|
|
585
631
|
font-weight: 500;
|
|
586
|
-
font-family:
|
|
632
|
+
font-family: var(--font-body);
|
|
587
633
|
}
|
|
588
634
|
[data-theme="dark"] .mega-about-desc,
|
|
589
635
|
[data-theme="dark"] .mega-about-sub {
|
|
@@ -626,8 +672,8 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
626
672
|
left: 50%;
|
|
627
673
|
transform: translateX(-50%);
|
|
628
674
|
min-width: 280px;
|
|
629
|
-
background:
|
|
630
|
-
border: 1px solid
|
|
675
|
+
background: var(--bg-surface);
|
|
676
|
+
border: 1px solid var(--border);
|
|
631
677
|
border-top: 2px solid var(--gold);
|
|
632
678
|
border-radius: 0 0 10px 10px;
|
|
633
679
|
box-shadow: 0 12px 32px rgba(0,0,0,0.35);
|
|
@@ -643,21 +689,21 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
643
689
|
align-items: center;
|
|
644
690
|
gap: 12px;
|
|
645
691
|
padding: 0.65rem 1.1rem;
|
|
646
|
-
color:
|
|
692
|
+
color: var(--muted);
|
|
647
693
|
text-decoration: none;
|
|
648
694
|
transition: background 0.12s, color 0.12s;
|
|
649
695
|
border-radius: 0;
|
|
650
696
|
}
|
|
651
697
|
.nav-dropdown li a:hover {
|
|
652
698
|
background: rgba(200,151,58,0.1);
|
|
653
|
-
color:
|
|
699
|
+
color: var(--fg);
|
|
654
700
|
}
|
|
655
701
|
.nav-dropdown li a:focus-visible {
|
|
656
702
|
outline: 2px solid var(--gold);
|
|
657
703
|
outline-offset: -2px;
|
|
658
704
|
}
|
|
659
705
|
.nav-dropdown li a:hover .drop-title { color: var(--gold); }
|
|
660
|
-
.nav-dropdown li a.active { color:
|
|
706
|
+
.nav-dropdown li a.active { color: var(--fg); }
|
|
661
707
|
.nav-dropdown li a.active .drop-title { color: var(--gold); }
|
|
662
708
|
.nav-dropdown li a.active .drop-icon { background: rgba(200,151,58,0.25); }
|
|
663
709
|
.nav-dropdown li + li {
|
|
@@ -683,16 +729,16 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
683
729
|
}
|
|
684
730
|
|
|
685
731
|
.drop-title {
|
|
686
|
-
font-family:
|
|
732
|
+
font-family: var(--font-body);
|
|
687
733
|
font-size: 0.8125rem;
|
|
688
734
|
font-weight: 600;
|
|
689
|
-
color:
|
|
735
|
+
color: var(--text-light);
|
|
690
736
|
line-height: 1.3;
|
|
691
737
|
transition: color 0.12s;
|
|
692
738
|
}
|
|
693
739
|
|
|
694
740
|
.drop-desc {
|
|
695
|
-
font-family:
|
|
741
|
+
font-family: var(--font-body);
|
|
696
742
|
font-size: 0.6875rem;
|
|
697
743
|
color: var(--text-subtle);
|
|
698
744
|
line-height: 1.3;
|
|
@@ -766,11 +812,11 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
766
812
|
padding: 1rem 3rem;
|
|
767
813
|
font-size: 0.9375rem;
|
|
768
814
|
font-weight: 300;
|
|
769
|
-
font-family:
|
|
815
|
+
font-family: var(--font-heading);
|
|
770
816
|
border-radius: 0;
|
|
771
817
|
text-align: left;
|
|
772
818
|
width: 100%;
|
|
773
|
-
color:
|
|
819
|
+
color: var(--fg);
|
|
774
820
|
transition: color 0.18s ease;
|
|
775
821
|
}
|
|
776
822
|
.nav-link::before {
|
|
@@ -785,9 +831,9 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
785
831
|
/* Last link (Calendar) sits directly above .nav-mobile-theme's own
|
|
786
832
|
border-top — drop its trailing divider so the two don't double up. */
|
|
787
833
|
.site-nav > .nav-link:last-of-type::before { display: none; }
|
|
788
|
-
.nav-link:hover { color:
|
|
834
|
+
.nav-link:hover { color: var(--gold); background: none; }
|
|
789
835
|
.nav-link::after { display: none; }
|
|
790
|
-
.nav-link.active { color:
|
|
836
|
+
.nav-link.active { color: var(--gold); font-weight: 400; }
|
|
791
837
|
|
|
792
838
|
.nav-mobile-theme {
|
|
793
839
|
display: flex;
|
|
@@ -831,19 +877,19 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
831
877
|
gap: 10px;
|
|
832
878
|
justify-content: flex-start;
|
|
833
879
|
font-size: 0.8rem;
|
|
834
|
-
color:
|
|
880
|
+
color: var(--fg);
|
|
835
881
|
transition: color 0.18s ease;
|
|
836
882
|
}
|
|
837
|
-
.nav-dropdown li a:hover { color:
|
|
883
|
+
.nav-dropdown li a:hover { color: var(--gold); background: none; }
|
|
838
884
|
.drop-icon { width: 24px; height: 24px; flex-shrink: 0; transition: color 0.18s ease; }
|
|
839
|
-
.drop-title { font-size: 0.75rem; color:
|
|
885
|
+
.drop-title { font-size: 0.75rem; color: var(--fg); transition: color 0.18s ease; }
|
|
840
886
|
[data-theme="dark"] .nav-dropdown li a { color: rgba(250,246,239,0.75); }
|
|
841
887
|
[data-theme="dark"] .nav-dropdown li a:hover,
|
|
842
|
-
[data-theme="dark"] .nav-dropdown li a:active { color:
|
|
888
|
+
[data-theme="dark"] .nav-dropdown li a:active { color: var(--gold); }
|
|
843
889
|
[data-theme="dark"] .nav-dropdown li a:hover .drop-title,
|
|
844
|
-
[data-theme="dark"] .nav-dropdown li a:active .drop-title { color:
|
|
890
|
+
[data-theme="dark"] .nav-dropdown li a:active .drop-title { color: var(--gold); }
|
|
845
891
|
[data-theme="dark"] .nav-dropdown li a:hover .drop-icon,
|
|
846
|
-
[data-theme="dark"] .nav-dropdown li a:active .drop-icon { color:
|
|
892
|
+
[data-theme="dark"] .nav-dropdown li a:active .drop-icon { color: var(--gold); }
|
|
847
893
|
[data-theme="dark"] .drop-title { color: rgba(250,246,239,0.9); }
|
|
848
894
|
.drop-desc { display: none; }
|
|
849
895
|
.nav-dropdown li + li { border-top: 1px solid rgba(255,255,255,0.04); }
|
|
@@ -937,6 +983,13 @@ const primaryNavLongestMatch = Math.max(-1, ...primaryNavMatchLengths);
|
|
|
937
983
|
|
|
938
984
|
const siteHeader = document.querySelector('.site-header') as HTMLElement | null;
|
|
939
985
|
|
|
986
|
+
// Shrink-on-scroll: condense the header once the page is scrolled.
|
|
987
|
+
if (siteHeader) {
|
|
988
|
+
const onScrollCompact = () => siteHeader.classList.toggle('is-compact', window.scrollY > 40);
|
|
989
|
+
onScrollCompact();
|
|
990
|
+
window.addEventListener('scroll', onScrollCompact, { passive: true });
|
|
991
|
+
}
|
|
992
|
+
|
|
940
993
|
function openMega() {
|
|
941
994
|
if (!megaNav || !aboutTrigger) return;
|
|
942
995
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
tone?: 'neutral' | 'accent';
|
|
5
|
+
}
|
|
6
|
+
const { text, tone = 'accent' } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<span class="b-badge" data-tone={tone}>{text}</span>
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
.b-badge {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
font-size: 0.75rem;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
letter-spacing: 0.06em;
|
|
17
|
+
text-transform: uppercase;
|
|
18
|
+
padding: 0.2rem 0.6rem;
|
|
19
|
+
border-radius: 999px;
|
|
20
|
+
}
|
|
21
|
+
.b-badge[data-tone='accent'] {
|
|
22
|
+
color: var(--gold);
|
|
23
|
+
background: var(--gold-light);
|
|
24
|
+
}
|
|
25
|
+
.b-badge[data-tone='neutral'] {
|
|
26
|
+
color: var(--muted);
|
|
27
|
+
background: var(--bg-section);
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Block wrapper over the shared Button primitive (single source). */
|
|
3
|
+
import Button from '../Button.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
variant?: 'primary' | 'secondary';
|
|
9
|
+
}
|
|
10
|
+
const { label, href, variant = 'primary' } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<Button href={href} variant={variant}>{label}</Button>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** A layout container: places child blocks in a responsive N-column grid
|
|
3
|
+
* (one column on mobile, N from tablet up). */
|
|
4
|
+
import BlockRenderer from '../BlockRenderer.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
columns?: 2 | 3 | 4;
|
|
8
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
9
|
+
blocks?: any[];
|
|
10
|
+
}
|
|
11
|
+
const { columns = 2, gap = 'md', blocks = [] } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="b-grid" data-cols={columns} data-gap={gap}>
|
|
15
|
+
<BlockRenderer blocks={blocks} />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
.b-grid {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: 1fr; /* mobile-first: single column */
|
|
22
|
+
}
|
|
23
|
+
.b-grid[data-gap='sm'] {
|
|
24
|
+
gap: 0.75rem;
|
|
25
|
+
}
|
|
26
|
+
.b-grid[data-gap='md'] {
|
|
27
|
+
gap: 1.5rem;
|
|
28
|
+
}
|
|
29
|
+
.b-grid[data-gap='lg'] {
|
|
30
|
+
gap: 3rem;
|
|
31
|
+
}
|
|
32
|
+
@media (min-width: 48em) {
|
|
33
|
+
.b-grid[data-cols='2'] {
|
|
34
|
+
grid-template-columns: repeat(2, 1fr);
|
|
35
|
+
}
|
|
36
|
+
.b-grid[data-cols='3'] {
|
|
37
|
+
grid-template-columns: repeat(3, 1fr);
|
|
38
|
+
}
|
|
39
|
+
.b-grid[data-cols='4'] {
|
|
40
|
+
grid-template-columns: repeat(2, 1fr);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
@media (min-width: 64em) {
|
|
44
|
+
.b-grid[data-cols='4'] {
|
|
45
|
+
grid-template-columns: repeat(4, 1fr);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
level?: number;
|
|
5
|
+
align?: 'start' | 'center' | 'end';
|
|
6
|
+
gradient?: boolean;
|
|
7
|
+
}
|
|
8
|
+
const { text, level = 2, align = 'start', gradient = false } = Astro.props;
|
|
9
|
+
const Tag = `h${level}` as unknown as keyof HTMLElementTagNameMap;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Tag class="b-heading" data-align={align} data-gradient={gradient ? 'true' : 'false'}>{text}</Tag>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
/* A heading owns its own alignment via the `align` prop — it never depends on
|
|
16
|
+
an ancestor rule. (Base no longer force-centres headings, so a recipe that
|
|
17
|
+
asks for `start` really is left-aligned, and `center` really is centred,
|
|
18
|
+
wherever the block is placed — including AI-authored block JSON.) */
|
|
19
|
+
.b-heading {
|
|
20
|
+
color: var(--fg);
|
|
21
|
+
margin: 0;
|
|
22
|
+
text-wrap: balance;
|
|
23
|
+
line-height: 1.2;
|
|
24
|
+
text-align: start;
|
|
25
|
+
}
|
|
26
|
+
.b-heading[data-align='center'] {
|
|
27
|
+
text-align: center;
|
|
28
|
+
}
|
|
29
|
+
.b-heading[data-align='end'] {
|
|
30
|
+
text-align: end;
|
|
31
|
+
}
|
|
32
|
+
/* Gradient text — only clip to text where supported, so it never renders as
|
|
33
|
+
invisible/transparent on older browsers (it stays solid ink there). */
|
|
34
|
+
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
|
|
35
|
+
.b-heading[data-gradient='true'] {
|
|
36
|
+
background-image: linear-gradient(120deg, var(--gold), var(--accent-2, var(--gold)));
|
|
37
|
+
-webkit-background-clip: text;
|
|
38
|
+
background-clip: text;
|
|
39
|
+
color: transparent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A basic image block with a small set of CANONICAL framing options — corners
|
|
4
|
+
* (none / soft / round), a drop shadow, an outline stroke, an aspect ratio, and
|
|
5
|
+
* an optional soft scroll-in reveal. These are clear, named choices the AI or a
|
|
6
|
+
* client picks from (not free-form CSS); corners/shadow/stroke read the global
|
|
7
|
+
* surface tokens, so images stay consistent with every other surface.
|
|
8
|
+
* The build-time optimisation pipeline (docs/IMAGE-PIPELINE.md) is a later
|
|
9
|
+
* upgrade — the schema stays the same.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
src: string;
|
|
13
|
+
alt?: string;
|
|
14
|
+
caption?: string;
|
|
15
|
+
radius?: 'none' | 'soft' | 'round';
|
|
16
|
+
shadow?: boolean;
|
|
17
|
+
border?: boolean;
|
|
18
|
+
ratio?: 'auto' | 'square' | '4/3' | '3/2' | '16/9' | '21/9';
|
|
19
|
+
reveal?: boolean;
|
|
20
|
+
}
|
|
21
|
+
const { src, alt = '', caption, radius = 'soft', shadow = false, border = false, ratio = 'auto', reveal = false } = Astro.props;
|
|
22
|
+
const ratioValue = ratio === 'auto' ? null : ratio === 'square' ? '1 / 1' : ratio.replace('/', ' / ');
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<figure
|
|
26
|
+
class="b-image"
|
|
27
|
+
data-radius={radius}
|
|
28
|
+
data-shadow={shadow ? 'true' : 'false'}
|
|
29
|
+
data-border={border ? 'true' : 'false'}
|
|
30
|
+
data-reveal={reveal ? 'true' : 'false'}
|
|
31
|
+
>
|
|
32
|
+
<img
|
|
33
|
+
src={src}
|
|
34
|
+
alt={alt}
|
|
35
|
+
loading="lazy"
|
|
36
|
+
decoding="async"
|
|
37
|
+
style={ratioValue ? `aspect-ratio: ${ratioValue}` : undefined}
|
|
38
|
+
/>
|
|
39
|
+
{caption && <figcaption>{caption}</figcaption>}
|
|
40
|
+
</figure>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.b-image { margin: 0; }
|
|
44
|
+
.b-image img {
|
|
45
|
+
display: block;
|
|
46
|
+
width: 100%;
|
|
47
|
+
height: auto;
|
|
48
|
+
object-fit: cover; /* only bites when an aspect-ratio is set */
|
|
49
|
+
}
|
|
50
|
+
/* Corners */
|
|
51
|
+
.b-image[data-radius='none'] img { border-radius: 0; }
|
|
52
|
+
.b-image[data-radius='soft'] img { border-radius: var(--radius); }
|
|
53
|
+
.b-image[data-radius='round'] img { border-radius: var(--radius-pill); }
|
|
54
|
+
/* Drop shadow + outline stroke, from the global surface tokens */
|
|
55
|
+
.b-image[data-shadow='true'] img { box-shadow: var(--shadow-md); }
|
|
56
|
+
.b-image[data-border='true'] img { border: var(--card-border); }
|
|
57
|
+
|
|
58
|
+
.b-image figcaption {
|
|
59
|
+
color: var(--muted);
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
margin-top: 0.5rem;
|
|
62
|
+
text-align: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Soft scroll-in reveal — native scroll-driven, so no JS/observer. Fully
|
|
66
|
+
visible where unsupported or under reduced-motion. */
|
|
67
|
+
@keyframes b-image-reveal {
|
|
68
|
+
from { clip-path: inset(42% 0 42% 0); opacity: 0.35; }
|
|
69
|
+
to { clip-path: inset(0 0 0 0); opacity: 1; }
|
|
70
|
+
}
|
|
71
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
72
|
+
@supports (animation-timeline: view()) {
|
|
73
|
+
.b-image[data-reveal='true'] img {
|
|
74
|
+
animation: b-image-reveal linear both;
|
|
75
|
+
animation-timeline: view();
|
|
76
|
+
animation-range: entry 5% cover 35%;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** List — a leaf primitive: a bulleted, numbered, or checked list of lines.
|
|
3
|
+
* `check` uses the shared check icon in the brand accent. Token-driven. */
|
|
4
|
+
import Icon from '../Icon.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
style?: 'bullet' | 'number' | 'check';
|
|
8
|
+
items?: string[];
|
|
9
|
+
}
|
|
10
|
+
const { style = 'bullet', items = [] } = Astro.props;
|
|
11
|
+
const Tag = style === 'number' ? 'ol' : 'ul';
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Tag class:list={['b-list', `b-list--${style}`]}>
|
|
15
|
+
{items.map((item) => (
|
|
16
|
+
<li class="b-list__item">
|
|
17
|
+
{style === 'check' && <span class="b-list__mark" aria-hidden="true"><Icon name="check" size={18} /></span>}
|
|
18
|
+
<span>{item}</span>
|
|
19
|
+
</li>
|
|
20
|
+
))}
|
|
21
|
+
</Tag>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.b-list {
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: 0.6rem;
|
|
30
|
+
color: var(--muted);
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
}
|
|
33
|
+
.b-list__item {
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 0.6rem;
|
|
36
|
+
align-items: flex-start;
|
|
37
|
+
}
|
|
38
|
+
/* bullet + number keep native markers, but as flex items we render our own */
|
|
39
|
+
.b-list--bullet {
|
|
40
|
+
list-style: none;
|
|
41
|
+
}
|
|
42
|
+
.b-list--bullet .b-list__item::before {
|
|
43
|
+
content: '';
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
width: 0.45rem;
|
|
46
|
+
height: 0.45rem;
|
|
47
|
+
margin-top: 0.6rem;
|
|
48
|
+
border-radius: 999px;
|
|
49
|
+
background: var(--gold);
|
|
50
|
+
}
|
|
51
|
+
.b-list--number {
|
|
52
|
+
list-style: none;
|
|
53
|
+
counter-reset: b-list;
|
|
54
|
+
}
|
|
55
|
+
.b-list--number .b-list__item {
|
|
56
|
+
counter-increment: b-list;
|
|
57
|
+
}
|
|
58
|
+
.b-list--number .b-list__item::before {
|
|
59
|
+
content: counter(b-list);
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
min-width: 1.5rem;
|
|
62
|
+
height: 1.5rem;
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
border-radius: 999px;
|
|
67
|
+
background: var(--gold-light);
|
|
68
|
+
color: var(--gold);
|
|
69
|
+
font-size: 0.8125rem;
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
}
|
|
72
|
+
.b-list__mark {
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
color: var(--gold);
|
|
76
|
+
margin-top: 0.15rem;
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
const { text } = Astro.props;
|
|
6
|
+
// Blank lines separate paragraphs. (Markdown rendering upgrades this later.)
|
|
7
|
+
const paragraphs = text
|
|
8
|
+
.split(/\n\s*\n/)
|
|
9
|
+
.map((p) => p.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="b-prose">
|
|
14
|
+
{paragraphs.map((p) => <p>{p}</p>)}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.b-prose {
|
|
19
|
+
color: var(--fg);
|
|
20
|
+
}
|
|
21
|
+
.b-prose p {
|
|
22
|
+
margin: 0 0 1rem;
|
|
23
|
+
line-height: 1.7;
|
|
24
|
+
max-width: var(--max);
|
|
25
|
+
}
|
|
26
|
+
.b-prose p:last-child {
|
|
27
|
+
margin-bottom: 0;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
cite?: string;
|
|
5
|
+
}
|
|
6
|
+
const { text, cite } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<figure class="b-quote">
|
|
10
|
+
<blockquote>{text}</blockquote>
|
|
11
|
+
{cite && <figcaption>— {cite}</figcaption>}
|
|
12
|
+
</figure>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.b-quote {
|
|
16
|
+
margin: 0;
|
|
17
|
+
border-inline-start: 3px solid var(--gold);
|
|
18
|
+
padding-inline-start: 1rem;
|
|
19
|
+
}
|
|
20
|
+
.b-quote blockquote {
|
|
21
|
+
margin: 0;
|
|
22
|
+
color: var(--fg);
|
|
23
|
+
font-style: italic;
|
|
24
|
+
font-size: 1.125rem;
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
}
|
|
27
|
+
.b-quote figcaption {
|
|
28
|
+
color: var(--muted);
|
|
29
|
+
margin-top: 0.5rem;
|
|
30
|
+
font-size: 0.9375rem;
|
|
31
|
+
}
|
|
32
|
+
</style>
|