@easy-web/content-blocks 1.0.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.
Files changed (54) hide show
  1. package/README.md +332 -0
  2. package/dist/components/__tests__/UniversalMedia.test.d.ts +2 -0
  3. package/dist/components/__tests__/UniversalMedia.test.d.ts.map +1 -0
  4. package/dist/components/__tests__/UniversalMedia.test.js +46 -0
  5. package/dist/components/__tests__/UniversalMedia.test.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/schemas/__tests__/notFound.test.d.ts +2 -0
  11. package/dist/schemas/__tests__/notFound.test.d.ts.map +1 -0
  12. package/dist/schemas/__tests__/notFound.test.js +29 -0
  13. package/dist/schemas/__tests__/notFound.test.js.map +1 -0
  14. package/dist/schemas/galleries.d.ts +319 -0
  15. package/dist/schemas/galleries.d.ts.map +1 -0
  16. package/dist/schemas/galleries.js +136 -0
  17. package/dist/schemas/galleries.js.map +1 -0
  18. package/dist/schemas/notFound.d.ts +35 -0
  19. package/dist/schemas/notFound.d.ts.map +1 -0
  20. package/dist/schemas/notFound.js +30 -0
  21. package/dist/schemas/notFound.js.map +1 -0
  22. package/package.json +51 -0
  23. package/src/components/.gitkeep +0 -0
  24. package/src/components/BlogPostCard.astro +79 -0
  25. package/src/components/Card.astro +70 -0
  26. package/src/components/CardGrid.astro +29 -0
  27. package/src/components/ContactSection.astro +73 -0
  28. package/src/components/CtaSection.astro +82 -0
  29. package/src/components/DraftBanner.astro +33 -0
  30. package/src/components/Footer.astro +81 -0
  31. package/src/components/GalleryCarousel.astro +296 -0
  32. package/src/components/GalleryFeatureHighlight.astro +153 -0
  33. package/src/components/GalleryHeroSlider.astro +362 -0
  34. package/src/components/GalleryImageGrid.astro +143 -0
  35. package/src/components/GalleryLightboxGrid.astro +353 -0
  36. package/src/components/GalleryMasonryGrid.astro +127 -0
  37. package/src/components/GallerySection.astro +69 -0
  38. package/src/components/Header.astro +210 -0
  39. package/src/components/HeaderCentered.astro +231 -0
  40. package/src/components/HeaderFlyout.astro +373 -0
  41. package/src/components/HeaderHideOnScroll.astro +237 -0
  42. package/src/components/Hero.astro +78 -0
  43. package/src/components/LanguageNotice.astro +32 -0
  44. package/src/components/LanguageSwitch.astro +67 -0
  45. package/src/components/LegalLayout.astro +53 -0
  46. package/src/components/NotFound.astro +124 -0
  47. package/src/components/Prose.astro +156 -0
  48. package/src/components/Section.astro +37 -0
  49. package/src/components/ThemeToggle.astro +82 -0
  50. package/src/components/UniversalMedia.astro +102 -0
  51. package/src/components/__tests__/UniversalMedia.test.ts +65 -0
  52. package/src/schemas/__tests__/notFound.test.ts +32 -0
  53. package/src/schemas/galleries.ts +152 -0
  54. package/src/schemas/notFound.ts +33 -0
@@ -0,0 +1,373 @@
1
+ ---
2
+ /**
3
+ * HeaderFlyout – responsive site header with dropdown flyout navigation.
4
+ * Pure Astro component with scoped vanilla JS for mobile and flyout behavior.
5
+ */
6
+ import type { NavItem } from '@easy-web/cms-adapters';
7
+ import ThemeToggle from './ThemeToggle.astro';
8
+ import LanguageSwitch from './LanguageSwitch.astro';
9
+
10
+ interface Props {
11
+ siteName: string;
12
+ navItems: NavItem[];
13
+ currentLang: string;
14
+ pathname: string;
15
+ /** Explicit alternate locale URL to pass to LanguageSwitch. */
16
+ alternateHref?: string;
17
+ /** Accessible label for the mobile menu toggle button. */
18
+ menuLabel?: string;
19
+ /** Optional logo URL. When provided, renders an <img> in the brand area instead of site name text. */
20
+ logo?: string;
21
+ }
22
+
23
+ const { siteName, navItems, currentLang, pathname, alternateHref, menuLabel = 'Toggle navigation menu', logo } = Astro.props;
24
+ const navId = `ew-header-flyout-nav-${Math.random().toString(36).slice(2, 9)}`;
25
+ const dropdownIdPrefix = `${navId}-dropdown`;
26
+ ---
27
+
28
+ <header class="ew-header-flyout" data-testid="header-flyout">
29
+ <div class="ew-header-flyout__inner">
30
+ <a href={currentLang === 'de' ? '/' : `/${currentLang}/`} class="ew-header-flyout__brand">
31
+ {logo ? <img src={logo} alt={siteName} class="ew-header-flyout__logo" /> : siteName}
32
+ </a>
33
+
34
+ <button
35
+ type="button"
36
+ class="ew-header-flyout__menu-btn"
37
+ aria-label={menuLabel}
38
+ aria-expanded="false"
39
+ aria-controls={navId}
40
+ data-testid="mobile-menu-toggle"
41
+ >
42
+ <svg class="ew-header-flyout__menu-icon ew-header-flyout__menu-icon--open" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
43
+ <line x1="3" y1="6" x2="21" y2="6"></line>
44
+ <line x1="3" y1="12" x2="21" y2="12"></line>
45
+ <line x1="3" y1="18" x2="21" y2="18"></line>
46
+ </svg>
47
+ <svg class="ew-header-flyout__menu-icon ew-header-flyout__menu-icon--close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
48
+ <line x1="18" y1="6" x2="6" y2="18"></line>
49
+ <line x1="6" y1="6" x2="18" y2="18"></line>
50
+ </svg>
51
+ </button>
52
+
53
+ <nav id={navId} class="ew-header-flyout__nav" aria-label="Main navigation">
54
+ <ul class="ew-header-flyout__links">
55
+ {navItems.map((item, index) => {
56
+ const hasChildren = Boolean(item.children && item.children.length > 0);
57
+ const dropdownId = `${dropdownIdPrefix}-${index}`;
58
+ return (
59
+ <li class="ew-header-flyout__item">
60
+ {hasChildren ? (
61
+ <div class="ew-header-flyout__dropdown-wrapper">
62
+ <button
63
+ type="button"
64
+ class:list={[
65
+ 'ew-header-flyout__link',
66
+ 'ew-header-flyout__trigger',
67
+ { 'ew-header-flyout__link--active': pathname === item.href }
68
+ ]}
69
+ aria-expanded="false"
70
+ aria-haspopup="true"
71
+ aria-controls={dropdownId}
72
+ >
73
+ <span>{item.label}</span>
74
+ <svg class="ew-header-flyout__chevron" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
75
+ <path d="M4 6l4 4 4-4"></path>
76
+ </svg>
77
+ </button>
78
+ <ul id={dropdownId} class="ew-header-flyout__dropdown" role="menu">
79
+ {item.children?.map((child) => (
80
+ <li class="ew-header-flyout__dropdown-item" role="none">
81
+ <a
82
+ href={child.href}
83
+ class:list={[
84
+ 'ew-header-flyout__dropdown-link',
85
+ { 'ew-header-flyout__dropdown-link--active': pathname === child.href }
86
+ ]}
87
+ role="menuitem"
88
+ >
89
+ {child.label}
90
+ </a>
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ </div>
95
+ ) : (
96
+ <a href={item.href} class:list={['ew-header-flyout__link', { 'ew-header-flyout__link--active': pathname === item.href }]}>
97
+ {item.label}
98
+ </a>
99
+ )}
100
+ </li>
101
+ );
102
+ })}
103
+ </ul>
104
+ <div class="ew-header-flyout__actions">
105
+ <slot name="actions">
106
+ <ThemeToggle />
107
+ <LanguageSwitch currentLang={currentLang} pathname={pathname} alternateHref={alternateHref} />
108
+ </slot>
109
+ </div>
110
+ </nav>
111
+ </div>
112
+ </header>
113
+
114
+ <style>
115
+ .ew-header-flyout {
116
+ position: sticky;
117
+ top: 0;
118
+ z-index: 100;
119
+ background: var(--ew-surface);
120
+ border-bottom: 1px solid var(--ew-border);
121
+ }
122
+ .ew-header-flyout__inner {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ max-width: 72rem;
127
+ margin: 0 auto;
128
+ padding: var(--ew-space-3) var(--ew-space-5);
129
+ }
130
+ .ew-header-flyout__brand {
131
+ font-family: var(--ew-font-sans);
132
+ font-size: var(--ew-text-lg);
133
+ font-weight: 700;
134
+ color: var(--ew-on-surface);
135
+ text-decoration: none;
136
+ letter-spacing: -0.01em;
137
+ }
138
+ .ew-header-flyout__brand:hover {
139
+ color: var(--ew-primary);
140
+ }
141
+ .ew-header-flyout__logo {
142
+ height: 1.5rem;
143
+ width: auto;
144
+ display: block;
145
+ }
146
+
147
+ .ew-header-flyout__menu-btn {
148
+ display: none;
149
+ background: transparent;
150
+ border: none;
151
+ color: var(--ew-on-surface);
152
+ cursor: pointer;
153
+ padding: var(--ew-space-1);
154
+ }
155
+ .ew-header-flyout__menu-btn:focus-visible,
156
+ .ew-header-flyout__link:focus-visible,
157
+ .ew-header-flyout__dropdown-link:focus-visible {
158
+ outline: 2px solid var(--ew-primary);
159
+ outline-offset: 2px;
160
+ }
161
+ .ew-header-flyout__menu-icon--close { display: none; }
162
+
163
+ .ew-header-flyout__nav {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: var(--ew-space-5);
167
+ }
168
+ .ew-header-flyout__links {
169
+ display: flex;
170
+ align-items: center;
171
+ list-style: none;
172
+ margin: 0;
173
+ padding: 0;
174
+ gap: var(--ew-space-1);
175
+ }
176
+ .ew-header-flyout__item {
177
+ margin: 0;
178
+ }
179
+ .ew-header-flyout__link {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: var(--ew-space-1);
183
+ padding: var(--ew-space-2) var(--ew-space-3);
184
+ border: none;
185
+ border-radius: var(--ew-radius-md);
186
+ background: transparent;
187
+ color: var(--ew-muted);
188
+ cursor: pointer;
189
+ font-family: var(--ew-font-sans);
190
+ font-size: var(--ew-text-sm);
191
+ font-weight: 500;
192
+ line-height: var(--ew-leading-normal);
193
+ text-decoration: none;
194
+ }
195
+ .ew-header-flyout__link:hover,
196
+ .ew-header-flyout__link[aria-expanded="true"] {
197
+ color: var(--ew-on-surface);
198
+ background: var(--ew-surface-muted);
199
+ }
200
+ .ew-header-flyout__link--active {
201
+ color: var(--ew-on-surface);
202
+ background: var(--ew-surface-muted);
203
+ }
204
+ .ew-header-flyout__trigger[aria-expanded="true"] .ew-header-flyout__chevron {
205
+ transform: rotate(180deg);
206
+ }
207
+ .ew-header-flyout__chevron {
208
+ flex: none;
209
+ }
210
+ .ew-header-flyout__dropdown-wrapper {
211
+ position: relative;
212
+ }
213
+ .ew-header-flyout__dropdown {
214
+ display: none;
215
+ position: absolute;
216
+ top: 100%;
217
+ left: 0;
218
+ min-width: 12rem;
219
+ background: var(--ew-surface);
220
+ border: 1px solid var(--ew-border);
221
+ border-radius: var(--ew-radius-md);
222
+ padding: var(--ew-space-2);
223
+ list-style: none;
224
+ margin: 0;
225
+ z-index: 200;
226
+ }
227
+ .ew-header-flyout__dropdown[data-open] {
228
+ display: block;
229
+ }
230
+ .ew-header-flyout__dropdown-link {
231
+ display: block;
232
+ padding: var(--ew-space-2) var(--ew-space-3);
233
+ border-radius: var(--ew-radius-sm);
234
+ color: var(--ew-muted);
235
+ font-family: var(--ew-font-sans);
236
+ font-size: var(--ew-text-sm);
237
+ font-weight: 500;
238
+ text-decoration: none;
239
+ }
240
+ .ew-header-flyout__dropdown-link:hover,
241
+ .ew-header-flyout__dropdown-link--active {
242
+ color: var(--ew-on-surface);
243
+ background: var(--ew-surface-muted);
244
+ }
245
+ .ew-header-flyout__actions {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: var(--ew-space-2);
249
+ border-left: 1px solid var(--ew-border);
250
+ padding-left: var(--ew-space-3);
251
+ margin-left: var(--ew-space-1);
252
+ }
253
+
254
+ @media (max-width: 768px) {
255
+ .ew-header-flyout__menu-btn {
256
+ display: flex;
257
+ }
258
+ .ew-header-flyout__nav {
259
+ display: none;
260
+ position: absolute;
261
+ top: 100%;
262
+ left: 0;
263
+ right: 0;
264
+ flex-direction: column;
265
+ align-items: stretch;
266
+ background: var(--ew-surface);
267
+ border-bottom: 1px solid var(--ew-border);
268
+ padding: var(--ew-space-4) var(--ew-space-5);
269
+ gap: var(--ew-space-4);
270
+ }
271
+ .ew-header-flyout__nav[data-open] {
272
+ display: flex;
273
+ }
274
+ .ew-header-flyout__links {
275
+ flex-direction: column;
276
+ align-items: stretch;
277
+ gap: var(--ew-space-1);
278
+ }
279
+ .ew-header-flyout__link {
280
+ width: 100%;
281
+ justify-content: space-between;
282
+ }
283
+ .ew-header-flyout__dropdown {
284
+ position: static;
285
+ min-width: 0;
286
+ margin-top: var(--ew-space-1);
287
+ border-radius: var(--ew-radius-md);
288
+ }
289
+ .ew-header-flyout__actions {
290
+ border-left: none;
291
+ padding-left: 0;
292
+ margin-left: 0;
293
+ padding-top: var(--ew-space-3);
294
+ border-top: 1px solid var(--ew-border);
295
+ }
296
+ .ew-header-flyout__menu-btn[aria-expanded="true"] .ew-header-flyout__menu-icon--open { display: none; }
297
+ .ew-header-flyout__menu-btn[aria-expanded="true"] .ew-header-flyout__menu-icon--close { display: block; }
298
+ }
299
+ </style>
300
+
301
+ <script>
302
+ function initHeader(headerEl: Element) {
303
+ const menuBtn = headerEl.querySelector('.ew-header-flyout__menu-btn') as HTMLButtonElement | null;
304
+ const nav = headerEl.querySelector('.ew-header-flyout__nav') as HTMLElement | null;
305
+
306
+ if (menuBtn && nav) {
307
+ menuBtn.addEventListener('click', () => {
308
+ const isOpen = menuBtn.getAttribute('aria-expanded') === 'true';
309
+ menuBtn.setAttribute('aria-expanded', String(!isOpen));
310
+ isOpen ? nav.removeAttribute('data-open') : nav.setAttribute('data-open', '');
311
+ });
312
+ }
313
+
314
+ const closeAllDropdowns = () => {
315
+ headerEl.querySelectorAll('.ew-header-flyout__trigger').forEach((trigger) => {
316
+ const wrapper = trigger.closest('.ew-header-flyout__dropdown-wrapper');
317
+ const dropdown = wrapper?.querySelector('.ew-header-flyout__dropdown');
318
+ trigger.setAttribute('aria-expanded', 'false');
319
+ dropdown?.removeAttribute('data-open');
320
+ });
321
+ };
322
+
323
+ headerEl.querySelectorAll('.ew-header-flyout__trigger').forEach((trigger) => {
324
+ const button = trigger as HTMLButtonElement;
325
+ const wrapper = button.closest('.ew-header-flyout__dropdown-wrapper');
326
+ const dropdown = wrapper?.querySelector('.ew-header-flyout__dropdown') as HTMLElement | null;
327
+ if (!wrapper || !dropdown) return;
328
+
329
+ const open = () => {
330
+ closeAllDropdowns();
331
+ button.setAttribute('aria-expanded', 'true');
332
+ dropdown.setAttribute('data-open', '');
333
+ };
334
+ const close = () => {
335
+ button.setAttribute('aria-expanded', 'false');
336
+ dropdown.removeAttribute('data-open');
337
+ };
338
+
339
+ wrapper.addEventListener('mouseenter', open);
340
+ wrapper.addEventListener('mouseleave', close);
341
+ wrapper.addEventListener('focusin', open);
342
+ wrapper.addEventListener('focusout', (event) => {
343
+ const nextTarget = event.relatedTarget as Node | null;
344
+ if (!nextTarget || !wrapper.contains(nextTarget)) close();
345
+ });
346
+
347
+ button.addEventListener('click', () => {
348
+ button.getAttribute('aria-expanded') === 'true' ? close() : open();
349
+ });
350
+ button.addEventListener('keydown', (event) => {
351
+ if (event.key === 'Enter') {
352
+ event.preventDefault();
353
+ open();
354
+ }
355
+ if (event.key === 'Escape') {
356
+ close();
357
+ }
358
+ });
359
+ dropdown.addEventListener('keydown', (event) => {
360
+ if (event.key === 'Escape') {
361
+ close();
362
+ button.focus();
363
+ }
364
+ });
365
+ });
366
+
367
+ document.addEventListener('click', (event) => {
368
+ if (!headerEl.contains(event.target as Node)) closeAllDropdowns();
369
+ });
370
+ }
371
+
372
+ document.querySelectorAll('.ew-header-flyout').forEach(initHeader);
373
+ </script>
@@ -0,0 +1,237 @@
1
+ ---
2
+ import ThemeToggle from './ThemeToggle.astro';
3
+ import LanguageSwitch from './LanguageSwitch.astro';
4
+
5
+ interface Props {
6
+ siteName: string;
7
+ navItems: Array<{ label: string; href: string }>;
8
+ currentLang: string;
9
+ pathname: string;
10
+ /** Explicit alternate locale URL to pass to LanguageSwitch. */
11
+ alternateHref?: string;
12
+ /** Accessible label for the mobile menu toggle button. */
13
+ menuLabel?: string;
14
+ /** Optional logo URL. When provided, renders an <img> in the brand area instead of site name text. */
15
+ logo?: string;
16
+ }
17
+
18
+ const { siteName, navItems, currentLang, pathname, alternateHref, menuLabel = 'Toggle navigation menu', logo } = Astro.props;
19
+ const navId = `ew-header-hos-nav-${Math.random().toString(36).slice(2, 9)}`;
20
+ ---
21
+
22
+ <header class="ew-header-hide-on-scroll" data-testid="header-hide-on-scroll">
23
+ <div class="ew-header-hide-on-scroll__inner">
24
+ <a href={currentLang === 'de' ? '/' : `/${currentLang}/`} class="ew-header-hide-on-scroll__brand">
25
+ {logo ? <img src={logo} alt={siteName} class="ew-header-hide-on-scroll__logo" /> : siteName}
26
+ </a>
27
+
28
+ <button
29
+ type="button"
30
+ class="ew-header-hide-on-scroll__menu-btn"
31
+ aria-label={menuLabel}
32
+ aria-expanded="false"
33
+ aria-controls={navId}
34
+ data-testid="mobile-menu-toggle"
35
+ >
36
+ <svg class="ew-header-hide-on-scroll__menu-icon ew-header-hide-on-scroll__menu-icon--open" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
37
+ <line x1="3" y1="6" x2="21" y2="6"></line>
38
+ <line x1="3" y1="12" x2="21" y2="12"></line>
39
+ <line x1="3" y1="18" x2="21" y2="18"></line>
40
+ </svg>
41
+ <svg class="ew-header-hide-on-scroll__menu-icon ew-header-hide-on-scroll__menu-icon--close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
42
+ <line x1="18" y1="6" x2="6" y2="18"></line>
43
+ <line x1="6" y1="6" x2="18" y2="18"></line>
44
+ </svg>
45
+ </button>
46
+
47
+ <nav id={navId} class="ew-header-hide-on-scroll__nav" aria-label="Main navigation">
48
+ <ul class="ew-header-hide-on-scroll__links">
49
+ {navItems.map((item) => (
50
+ <li>
51
+ <a href={item.href} class:list={['ew-header-hide-on-scroll__link', { 'ew-header-hide-on-scroll__link--active': pathname === item.href }]}>
52
+ {item.label}
53
+ </a>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ <div class="ew-header-hide-on-scroll__actions">
58
+ <slot name="actions">
59
+ <ThemeToggle />
60
+ <LanguageSwitch currentLang={currentLang} pathname={pathname} alternateHref={alternateHref} />
61
+ </slot>
62
+ </div>
63
+ </nav>
64
+ </div>
65
+ </header>
66
+
67
+ <style>
68
+ .ew-header-hide-on-scroll {
69
+ position: fixed;
70
+ top: 0;
71
+ left: 0;
72
+ right: 0;
73
+ z-index: 100;
74
+ transition: transform 0.3s ease;
75
+ background: var(--ew-surface);
76
+ border-bottom: 1px solid var(--ew-border);
77
+ }
78
+ .ew-header-hide-on-scroll--hidden {
79
+ transform: translateY(-100%);
80
+ }
81
+ .ew-header-hide-on-scroll__inner {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ max-width: 72rem;
86
+ margin: 0 auto;
87
+ padding: var(--ew-space-3) var(--ew-space-5);
88
+ }
89
+ .ew-header-hide-on-scroll__brand {
90
+ font-family: var(--ew-font-sans);
91
+ font-size: var(--ew-text-lg);
92
+ font-weight: 700;
93
+ color: var(--ew-on-surface);
94
+ text-decoration: none;
95
+ letter-spacing: -0.01em;
96
+ }
97
+ .ew-header-hide-on-scroll__brand:hover {
98
+ color: var(--ew-primary);
99
+ }
100
+ .ew-header-hide-on-scroll__logo {
101
+ height: 1.5rem;
102
+ width: auto;
103
+ display: block;
104
+ }
105
+
106
+ .ew-header-hide-on-scroll__menu-btn {
107
+ display: none;
108
+ background: transparent;
109
+ border: none;
110
+ color: var(--ew-on-surface);
111
+ cursor: pointer;
112
+ padding: var(--ew-space-1);
113
+ }
114
+ .ew-header-hide-on-scroll__menu-btn:focus-visible {
115
+ outline: 2px solid var(--ew-primary);
116
+ outline-offset: 2px;
117
+ }
118
+ .ew-header-hide-on-scroll__menu-icon--close { display: none; }
119
+
120
+ .ew-header-hide-on-scroll__nav {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: var(--ew-space-5);
124
+ }
125
+ .ew-header-hide-on-scroll__links {
126
+ display: flex;
127
+ list-style: none;
128
+ margin: 0;
129
+ padding: 0;
130
+ gap: var(--ew-space-1);
131
+ }
132
+ .ew-header-hide-on-scroll__link {
133
+ display: block;
134
+ padding: var(--ew-space-2) var(--ew-space-3);
135
+ border-radius: var(--ew-radius-md);
136
+ color: var(--ew-muted);
137
+ font-family: var(--ew-font-sans);
138
+ font-size: var(--ew-text-sm);
139
+ font-weight: 500;
140
+ text-decoration: none;
141
+ }
142
+ .ew-header-hide-on-scroll__link:hover {
143
+ color: var(--ew-on-surface);
144
+ background: var(--ew-surface-muted);
145
+ }
146
+ .ew-header-hide-on-scroll__link--active {
147
+ color: var(--ew-on-surface);
148
+ background: var(--ew-surface-muted);
149
+ }
150
+ .ew-header-hide-on-scroll__actions {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: var(--ew-space-2);
154
+ border-left: 1px solid var(--ew-border);
155
+ padding-left: var(--ew-space-3);
156
+ margin-left: var(--ew-space-1);
157
+ }
158
+
159
+ @media (max-width: 768px) {
160
+ .ew-header-hide-on-scroll__menu-btn {
161
+ display: flex;
162
+ }
163
+ .ew-header-hide-on-scroll__nav {
164
+ display: none;
165
+ position: absolute;
166
+ top: 100%;
167
+ left: 0;
168
+ right: 0;
169
+ flex-direction: column;
170
+ background: var(--ew-surface);
171
+ border-bottom: 1px solid var(--ew-border);
172
+ padding: var(--ew-space-4) var(--ew-space-5);
173
+ gap: var(--ew-space-4);
174
+ }
175
+ .ew-header-hide-on-scroll__nav[data-open] {
176
+ display: flex;
177
+ }
178
+ .ew-header-hide-on-scroll__links {
179
+ flex-direction: column;
180
+ gap: var(--ew-space-1);
181
+ }
182
+ .ew-header-hide-on-scroll__actions {
183
+ border-left: none;
184
+ padding-left: 0;
185
+ margin-left: 0;
186
+ padding-top: var(--ew-space-3);
187
+ border-top: 1px solid var(--ew-border);
188
+ }
189
+ .ew-header-hide-on-scroll__menu-btn[aria-expanded="true"] .ew-header-hide-on-scroll__menu-icon--open { display: none; }
190
+ .ew-header-hide-on-scroll__menu-btn[aria-expanded="true"] .ew-header-hide-on-scroll__menu-icon--close { display: block; }
191
+ }
192
+ </style>
193
+
194
+ <script>
195
+ function initHeader(headerEl: Element) {
196
+ const applyPadding = () => {
197
+ document.body.style.paddingTop = `${headerEl.getBoundingClientRect().height}px`;
198
+ };
199
+ applyPadding();
200
+
201
+ let lastScrollY = window.scrollY;
202
+ let ticking = false;
203
+
204
+ const handleScroll = () => {
205
+ if (!ticking) {
206
+ window.requestAnimationFrame(() => {
207
+ const currentScrollY = window.scrollY;
208
+ if (currentScrollY > lastScrollY && currentScrollY > 80) {
209
+ headerEl.classList.add('ew-header-hide-on-scroll--hidden');
210
+ } else {
211
+ headerEl.classList.remove('ew-header-hide-on-scroll--hidden');
212
+ }
213
+ lastScrollY = currentScrollY;
214
+ ticking = false;
215
+ });
216
+ ticking = true;
217
+ }
218
+ };
219
+
220
+ window.addEventListener('scroll', handleScroll, { passive: true });
221
+ window.addEventListener('resize', applyPadding);
222
+
223
+ const resizeObserver = 'ResizeObserver' in window ? new ResizeObserver(applyPadding) : null;
224
+ resizeObserver?.observe(headerEl);
225
+
226
+ const btn = headerEl.querySelector('.ew-header-hide-on-scroll__menu-btn') as HTMLButtonElement | null;
227
+ const nav = headerEl.querySelector('.ew-header-hide-on-scroll__nav') as HTMLElement | null;
228
+ if (btn && nav) {
229
+ btn.addEventListener('click', () => {
230
+ const isOpen = btn.getAttribute('aria-expanded') === 'true';
231
+ btn.setAttribute('aria-expanded', String(!isOpen));
232
+ isOpen ? nav.removeAttribute('data-open') : nav.setAttribute('data-open', '');
233
+ });
234
+ }
235
+ }
236
+ document.querySelectorAll('.ew-header-hide-on-scroll').forEach(initHeader);
237
+ </script>
@@ -0,0 +1,78 @@
1
+ ---
2
+ /**
3
+ * Hero – prominent page banner with title, optional subtitle, and optional CTA.
4
+ * Pure Astro component using --ew-* design tokens.
5
+ */
6
+ interface Props {
7
+ title: string;
8
+ subtitle?: string;
9
+ ctaLabel?: string;
10
+ ctaHref?: string;
11
+ variant?: 'centered' | 'left-aligned';
12
+ }
13
+
14
+ const { title, subtitle, ctaLabel, ctaHref, variant = 'centered' } = Astro.props;
15
+ ---
16
+
17
+ <section class:list={['ew-hero', `ew-hero--${variant}`]}>
18
+ <div class="ew-hero__inner">
19
+ <h1 class="ew-hero__title">{title}</h1>
20
+ {subtitle && <p class="ew-hero__subtitle">{subtitle}</p>}
21
+ {ctaLabel && ctaHref && (
22
+ <a href={ctaHref} class="ew-hero__cta">{ctaLabel}</a>
23
+ )}
24
+ </div>
25
+ </section>
26
+
27
+ <style>
28
+ .ew-hero {
29
+ background: var(--ew-surface-muted);
30
+ padding: var(--ew-space-9) var(--ew-space-5);
31
+ }
32
+ .ew-hero__inner {
33
+ max-width: 72rem;
34
+ margin: 0 auto;
35
+ }
36
+ .ew-hero--centered {
37
+ text-align: center;
38
+ }
39
+ .ew-hero--centered .ew-hero__inner {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ }
44
+ .ew-hero--left-aligned {
45
+ text-align: left;
46
+ }
47
+ .ew-hero__title {
48
+ font-family: var(--ew-font-sans);
49
+ font-size: var(--ew-text-3xl);
50
+ font-weight: 800;
51
+ line-height: var(--ew-leading-tight);
52
+ color: var(--ew-on-surface);
53
+ margin: 0;
54
+ }
55
+ .ew-hero__subtitle {
56
+ font-family: var(--ew-font-sans);
57
+ font-size: var(--ew-text-xl);
58
+ line-height: var(--ew-leading-normal);
59
+ color: var(--ew-muted);
60
+ margin: var(--ew-space-4) 0 0;
61
+ max-width: 40rem;
62
+ }
63
+ .ew-hero__cta {
64
+ display: inline-block;
65
+ margin-top: var(--ew-space-6);
66
+ padding: var(--ew-space-3) var(--ew-space-6);
67
+ background: var(--ew-primary);
68
+ color: var(--ew-on-primary);
69
+ font-family: var(--ew-font-sans);
70
+ font-size: var(--ew-text-base);
71
+ font-weight: 600;
72
+ text-decoration: none;
73
+ border-radius: var(--ew-radius-md);
74
+ }
75
+ .ew-hero__cta:hover {
76
+ opacity: 0.9;
77
+ }
78
+ </style>