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.
Files changed (70) hide show
  1. package/LICENSE +102 -201
  2. package/LICENSE-Apache-2.0 +201 -0
  3. package/NOTICE +24 -5
  4. package/README.md +89 -73
  5. package/bin/check-thin.mjs +74 -0
  6. package/components/BlockRenderer.astro +89 -0
  7. package/components/Button.astro +85 -33
  8. package/components/CalendarEmbed.astro +188 -0
  9. package/components/Card.astro +7 -17
  10. package/components/Icon.astro +35 -0
  11. package/components/PostArticle.astro +133 -0
  12. package/components/PostCard.astro +93 -225
  13. package/components/SiteFooter.astro +2 -2
  14. package/components/SiteHeader.astro +104 -51
  15. package/components/blocks/Badge.astro +29 -0
  16. package/components/blocks/ButtonBlock.astro +13 -0
  17. package/components/blocks/Divider.astro +12 -0
  18. package/components/blocks/Grid.astro +48 -0
  19. package/components/blocks/Heading.astro +42 -0
  20. package/components/blocks/ImageBlock.astro +80 -0
  21. package/components/blocks/List.astro +78 -0
  22. package/components/blocks/Prose.astro +29 -0
  23. package/components/blocks/Quote.astro +32 -0
  24. package/components/blocks/Section.astro +52 -0
  25. package/components/blocks/Spacer.astro +20 -0
  26. package/components/blocks/Stack.astro +30 -0
  27. package/components/blocks/Stat.astro +30 -0
  28. package/components/recipes/Accordion.astro +84 -0
  29. package/components/recipes/Announcement.astro +74 -0
  30. package/components/recipes/Banner.astro +134 -0
  31. package/components/recipes/Bento.astro +138 -0
  32. package/components/recipes/ContactForm.astro +172 -0
  33. package/components/recipes/Cta.astro +83 -0
  34. package/components/recipes/Faq.astro +80 -0
  35. package/components/recipes/FeatureCards.astro +105 -0
  36. package/components/recipes/Gallery.astro +55 -0
  37. package/components/recipes/Hero.astro +191 -0
  38. package/components/recipes/Logos.astro +77 -0
  39. package/components/recipes/Marquee.astro +75 -0
  40. package/components/recipes/Pricing.astro +149 -0
  41. package/components/recipes/Stats.astro +124 -0
  42. package/components/recipes/Steps.astro +120 -0
  43. package/components/recipes/Tabs.astro +133 -0
  44. package/components/recipes/Testimonial.astro +66 -0
  45. package/components/recipes/Tiles.astro +248 -0
  46. package/content/blocks.ts +534 -0
  47. package/content/calendar.ts +40 -0
  48. package/content/collections.ts +25 -14
  49. package/content/posts.ts +89 -0
  50. package/content/schemas.ts +58 -123
  51. package/layouts/Base.astro +30 -56
  52. package/lib/calendar.ts +12 -0
  53. package/lib/icons.ts +64 -0
  54. package/lib/pages.ts +42 -0
  55. package/lib/posts.ts +93 -0
  56. package/lib/siteSettings.ts +18 -36
  57. package/lib/themeTokens.ts +163 -0
  58. package/package.json +61 -48
  59. package/styles/theme.css +42 -0
  60. package/components/DocLayout.astro +0 -292
  61. package/components/GroupCard.astro +0 -164
  62. package/components/LatestPostList.astro +0 -64
  63. package/components/PaginationNav.astro +0 -81
  64. package/components/PostsToolbar.astro +0 -83
  65. package/content/filters.ts +0 -107
  66. package/lib/mailLinks.ts +0 -13
  67. package/lib/mapsLink.ts +0 -7
  68. package/utils/excerpt.ts +0 -56
  69. package/utils/formatDate.ts +0 -22
  70. package/utils/freshness.ts +0 -5
@@ -0,0 +1,66 @@
1
+ ---
2
+ /** Testimonial — a Level-2 recipe: a centred quotation with author, optional
3
+ * role, and optional avatar. */
4
+ interface Props {
5
+ quote: string;
6
+ author: string;
7
+ role?: string;
8
+ avatar?: { src: string; alt?: string };
9
+ }
10
+ const { quote, author, role, avatar } = Astro.props;
11
+ ---
12
+
13
+ <figure class="b-testimonial">
14
+ <blockquote class="b-testimonial__quote">{`“${quote}”`}</blockquote>
15
+ <figcaption class="b-testimonial__by">
16
+ {avatar && <img class="b-testimonial__avatar" src={avatar.src} alt={avatar.alt ?? ''} loading="lazy" decoding="async" />}
17
+ <span class="b-testimonial__meta">
18
+ <span class="b-testimonial__author">{author}</span>
19
+ {role && <span class="b-testimonial__role">{role}</span>}
20
+ </span>
21
+ </figcaption>
22
+ </figure>
23
+
24
+ <style>
25
+ .b-testimonial {
26
+ max-width: var(--max);
27
+ margin-inline: auto;
28
+ padding-inline: 1rem;
29
+ text-align: center;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 1.25rem;
33
+ align-items: center;
34
+ }
35
+ .b-testimonial__quote {
36
+ margin: 0;
37
+ font-size: 1.375rem;
38
+ line-height: 1.5;
39
+ color: var(--fg);
40
+ font-style: italic;
41
+ }
42
+ .b-testimonial__by {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ gap: 0.75rem;
46
+ }
47
+ .b-testimonial__avatar {
48
+ width: 44px;
49
+ height: 44px;
50
+ border-radius: 999px;
51
+ object-fit: cover;
52
+ }
53
+ .b-testimonial__meta {
54
+ display: flex;
55
+ flex-direction: column;
56
+ text-align: left;
57
+ }
58
+ .b-testimonial__author {
59
+ font-weight: 600;
60
+ color: var(--fg);
61
+ }
62
+ .b-testimonial__role {
63
+ color: var(--muted);
64
+ font-size: 0.875rem;
65
+ }
66
+ </style>
@@ -0,0 +1,248 @@
1
+ ---
2
+ /**
3
+ * Tiles — a Level-2 recipe: a responsive grid of tiles in one of five looks,
4
+ * chosen with `variant`:
5
+ * outline — stroke only, no shadow (the "wireframe" look)
6
+ * elevated — soft shadow, lifts on hover
7
+ * filled — soft brand-tinted background
8
+ * gradient — brand → derived accent-2 gradient, light text
9
+ * image — full-bleed photo with a legibility scrim, light text
10
+ *
11
+ * Radius, stroke and elevation come from the GLOBAL surface tokens (--radius,
12
+ * --card-border, --shadow-*), so tiles stay consistent with every other card and
13
+ * a single knob restyles them all. A tile with `href` becomes one big link.
14
+ * Composes the Icon primitive.
15
+ */
16
+ import Heading from '../blocks/Heading.astro';
17
+ import Icon from '../Icon.astro';
18
+ import { isIconKey } from '../../lib/icons';
19
+
20
+ interface Tile {
21
+ icon?: string;
22
+ eyebrow?: string;
23
+ title: string;
24
+ text?: string;
25
+ href?: string;
26
+ image?: { src: string; alt?: string };
27
+ }
28
+ interface Props {
29
+ title?: string;
30
+ intro?: string;
31
+ columns?: 2 | 3 | 4;
32
+ variant?: 'outline' | 'elevated' | 'filled' | 'gradient' | 'image';
33
+ spotlight?: boolean;
34
+ tiles?: Tile[];
35
+ }
36
+ const { title, intro, columns = 3, variant = 'elevated', spotlight = false, tiles = [] } = Astro.props;
37
+ ---
38
+
39
+ <section class="b-tiles">
40
+ {(title || intro) && (
41
+ <div class="b-tiles__head">
42
+ {title && <Heading text={title} level={2} align="center" />}
43
+ {intro && <p class="b-tiles__intro">{intro}</p>}
44
+ </div>
45
+ )}
46
+ <div class="b-tiles__grid" data-cols={columns} data-variant={variant} data-spotlight={spotlight ? 'true' : 'false'}>
47
+ {tiles.map((tile) => {
48
+ const Tag = tile.href ? 'a' : 'div';
49
+ const bg =
50
+ variant === 'image' && tile.image
51
+ ? `--tile-image: url('${tile.image.src}')`
52
+ : undefined;
53
+ return (
54
+ <Tag class="b-tile" href={tile.href} style={bg}>
55
+ {tile.icon && isIconKey(tile.icon) && (
56
+ <span class="b-tile__icon"><Icon name={tile.icon} size={26} /></span>
57
+ )}
58
+ {tile.eyebrow && <span class="b-tile__eyebrow">{tile.eyebrow}</span>}
59
+ <h3 class="b-tile__title">{tile.title}</h3>
60
+ {tile.text && <p class="b-tile__text">{tile.text}</p>}
61
+ {tile.href && <span class="b-tile__more" aria-hidden="true">Learn more &rarr;</span>}
62
+ </Tag>
63
+ );
64
+ })}
65
+ </div>
66
+ </section>
67
+
68
+ <style>
69
+ .b-tiles {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 2rem;
73
+ max-width: 72rem;
74
+ margin-inline: auto;
75
+ padding-inline: 1rem;
76
+ }
77
+ .b-tiles__head {
78
+ max-width: var(--max);
79
+ margin-inline: auto;
80
+ text-align: center;
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 0.75rem;
84
+ }
85
+ .b-tiles__intro {
86
+ margin: 0;
87
+ color: var(--muted);
88
+ font-size: 1.0625rem;
89
+ line-height: 1.6;
90
+ }
91
+ .b-tiles__grid {
92
+ display: grid;
93
+ grid-template-columns: 1fr;
94
+ gap: 1.25rem;
95
+ }
96
+ @media (min-width: 48em) {
97
+ .b-tiles__grid[data-cols='2'] { grid-template-columns: repeat(2, 1fr); }
98
+ .b-tiles__grid[data-cols='3'] { grid-template-columns: repeat(3, 1fr); }
99
+ .b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(2, 1fr); }
100
+ }
101
+ @media (min-width: 64em) {
102
+ .b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(4, 1fr); }
103
+ }
104
+
105
+ /* Base tile — radius/stroke/shadow all from the global surface tokens. */
106
+ .b-tile {
107
+ position: relative;
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 0.5rem;
111
+ padding: 1.5rem;
112
+ border-radius: var(--radius);
113
+ background: var(--bg-surface);
114
+ color: var(--fg);
115
+ text-decoration: none;
116
+ overflow: hidden;
117
+ isolation: isolate;
118
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
119
+ }
120
+
121
+ /* ── Variants ── */
122
+ .b-tiles__grid[data-variant='outline'] .b-tile {
123
+ border: var(--card-border);
124
+ }
125
+ .b-tiles__grid[data-variant='elevated'] .b-tile {
126
+ box-shadow: var(--shadow-md);
127
+ border: var(--card-border);
128
+ }
129
+ .b-tiles__grid[data-variant='filled'] .b-tile {
130
+ background: var(--gold-light);
131
+ }
132
+ .b-tiles__grid[data-variant='gradient'] .b-tile {
133
+ background: linear-gradient(135deg, var(--gold), var(--accent-2, var(--gold)));
134
+ color: var(--text-light);
135
+ }
136
+ .b-tiles__grid[data-variant='image'] .b-tile {
137
+ color: #fff;
138
+ min-height: 15rem;
139
+ justify-content: flex-end;
140
+ background: var(--bg-section);
141
+ }
142
+ .b-tiles__grid[data-variant='image'] .b-tile::before {
143
+ content: '';
144
+ position: absolute;
145
+ inset: 0;
146
+ z-index: -1;
147
+ background-image: linear-gradient(to top, rgba(10, 12, 20, 0.85), rgba(10, 12, 20, 0.15)), var(--tile-image);
148
+ background-size: cover;
149
+ background-position: center;
150
+ }
151
+
152
+ /* Hover: only tiles that link lift. */
153
+ a.b-tile:hover {
154
+ transform: translateY(-4px);
155
+ }
156
+ .b-tiles__grid[data-variant='elevated'] a.b-tile:hover {
157
+ box-shadow: var(--shadow-lg);
158
+ }
159
+ .b-tiles__grid[data-variant='outline'] a.b-tile:hover {
160
+ border-color: var(--gold);
161
+ }
162
+ a.b-tile:focus-visible {
163
+ outline: 2px solid var(--gold);
164
+ outline-offset: 2px;
165
+ }
166
+ @media (prefers-reduced-motion: reduce) {
167
+ .b-tile { transition: none; }
168
+ a.b-tile:hover { transform: none; }
169
+ }
170
+
171
+ .b-tile__icon {
172
+ display: inline-flex;
173
+ color: var(--gold);
174
+ margin-bottom: 0.25rem;
175
+ }
176
+ .b-tiles__grid[data-variant='gradient'] .b-tile__icon,
177
+ .b-tiles__grid[data-variant='image'] .b-tile__icon {
178
+ color: currentColor;
179
+ }
180
+ .b-tile__eyebrow {
181
+ font-size: 0.75rem;
182
+ font-weight: 700;
183
+ letter-spacing: 0.1em;
184
+ text-transform: uppercase;
185
+ color: var(--gold);
186
+ }
187
+ .b-tiles__grid[data-variant='gradient'] .b-tile__eyebrow,
188
+ .b-tiles__grid[data-variant='image'] .b-tile__eyebrow {
189
+ color: currentColor;
190
+ opacity: 0.85;
191
+ }
192
+ .b-tile__title {
193
+ margin: 0;
194
+ font-size: 1.2rem;
195
+ color: inherit;
196
+ }
197
+ .b-tile__text {
198
+ margin: 0;
199
+ color: var(--muted);
200
+ line-height: 1.6;
201
+ }
202
+ .b-tiles__grid[data-variant='gradient'] .b-tile__text,
203
+ .b-tiles__grid[data-variant='image'] .b-tile__text,
204
+ .b-tiles__grid[data-variant='filled'] .b-tile__text {
205
+ color: inherit;
206
+ opacity: 0.92;
207
+ }
208
+ .b-tile__more {
209
+ margin-top: auto;
210
+ padding-top: 0.5rem;
211
+ font-size: 0.85rem;
212
+ font-weight: 600;
213
+ color: var(--gold);
214
+ }
215
+ .b-tiles__grid[data-variant='gradient'] .b-tile__more,
216
+ .b-tiles__grid[data-variant='image'] .b-tile__more {
217
+ color: currentColor;
218
+ }
219
+
220
+ /* Spotlight — a soft glow that follows the pointer on hover (opt-in). The glow
221
+ sits above the surface but below the content, so text stays crisp. */
222
+ .b-tiles__grid[data-spotlight='true'] .b-tile > * { position: relative; z-index: 1; }
223
+ .b-tiles__grid[data-spotlight='true'] .b-tile::after {
224
+ content: '';
225
+ position: absolute;
226
+ inset: 0;
227
+ z-index: 0;
228
+ border-radius: inherit;
229
+ background: radial-gradient(18rem 18rem at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--gold) 22%, transparent), transparent 60%);
230
+ opacity: 0;
231
+ transition: opacity 0.25s ease;
232
+ pointer-events: none;
233
+ }
234
+ .b-tiles__grid[data-spotlight='true'] .b-tile:hover::after { opacity: 1; }
235
+ </style>
236
+
237
+ <script>
238
+ // Spotlight: track the pointer over opt-in tiles and drive the glow position.
239
+ if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
240
+ document.querySelectorAll<HTMLElement>('.b-tiles__grid[data-spotlight="true"] .b-tile').forEach((tile) => {
241
+ tile.addEventListener('pointermove', (e) => {
242
+ const r = tile.getBoundingClientRect();
243
+ tile.style.setProperty('--mx', `${((e.clientX - r.left) / r.width) * 100}%`);
244
+ tile.style.setProperty('--my', `${((e.clientY - r.top) / r.height) * 100}%`);
245
+ });
246
+ });
247
+ }
248
+ </script>