@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.
- package/README.md +332 -0
- package/dist/components/__tests__/UniversalMedia.test.d.ts +2 -0
- package/dist/components/__tests__/UniversalMedia.test.d.ts.map +1 -0
- package/dist/components/__tests__/UniversalMedia.test.js +46 -0
- package/dist/components/__tests__/UniversalMedia.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/__tests__/notFound.test.d.ts +2 -0
- package/dist/schemas/__tests__/notFound.test.d.ts.map +1 -0
- package/dist/schemas/__tests__/notFound.test.js +29 -0
- package/dist/schemas/__tests__/notFound.test.js.map +1 -0
- package/dist/schemas/galleries.d.ts +319 -0
- package/dist/schemas/galleries.d.ts.map +1 -0
- package/dist/schemas/galleries.js +136 -0
- package/dist/schemas/galleries.js.map +1 -0
- package/dist/schemas/notFound.d.ts +35 -0
- package/dist/schemas/notFound.d.ts.map +1 -0
- package/dist/schemas/notFound.js +30 -0
- package/dist/schemas/notFound.js.map +1 -0
- package/package.json +51 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/BlogPostCard.astro +79 -0
- package/src/components/Card.astro +70 -0
- package/src/components/CardGrid.astro +29 -0
- package/src/components/ContactSection.astro +73 -0
- package/src/components/CtaSection.astro +82 -0
- package/src/components/DraftBanner.astro +33 -0
- package/src/components/Footer.astro +81 -0
- package/src/components/GalleryCarousel.astro +296 -0
- package/src/components/GalleryFeatureHighlight.astro +153 -0
- package/src/components/GalleryHeroSlider.astro +362 -0
- package/src/components/GalleryImageGrid.astro +143 -0
- package/src/components/GalleryLightboxGrid.astro +353 -0
- package/src/components/GalleryMasonryGrid.astro +127 -0
- package/src/components/GallerySection.astro +69 -0
- package/src/components/Header.astro +210 -0
- package/src/components/HeaderCentered.astro +231 -0
- package/src/components/HeaderFlyout.astro +373 -0
- package/src/components/HeaderHideOnScroll.astro +237 -0
- package/src/components/Hero.astro +78 -0
- package/src/components/LanguageNotice.astro +32 -0
- package/src/components/LanguageSwitch.astro +67 -0
- package/src/components/LegalLayout.astro +53 -0
- package/src/components/NotFound.astro +124 -0
- package/src/components/Prose.astro +156 -0
- package/src/components/Section.astro +37 -0
- package/src/components/ThemeToggle.astro +82 -0
- package/src/components/UniversalMedia.astro +102 -0
- package/src/components/__tests__/UniversalMedia.test.ts +65 -0
- package/src/schemas/__tests__/notFound.test.ts +32 -0
- package/src/schemas/galleries.ts +152 -0
- package/src/schemas/notFound.ts +33 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LanguageNotice – subtle info box for language/binding disclaimers.
|
|
4
|
+
* Pure Astro component using --ew-* design tokens.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { message } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="ew-language-notice">
|
|
14
|
+
<p class="ew-language-notice__text">{message}</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.ew-language-notice {
|
|
19
|
+
background: var(--ew-surface-muted);
|
|
20
|
+
border: 1px solid var(--ew-border);
|
|
21
|
+
border-radius: var(--ew-radius-md);
|
|
22
|
+
padding: var(--ew-space-3) var(--ew-space-4);
|
|
23
|
+
margin: 0 0 var(--ew-space-5);
|
|
24
|
+
}
|
|
25
|
+
.ew-language-notice__text {
|
|
26
|
+
font-family: var(--ew-font-sans);
|
|
27
|
+
font-size: var(--ew-text-sm);
|
|
28
|
+
color: var(--ew-muted);
|
|
29
|
+
margin: 0;
|
|
30
|
+
line-height: var(--ew-leading-normal);
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LanguageSwitch – links to the alternate locale version of the current page.
|
|
4
|
+
* Convention: default locale (e.g. "de") lives at `/`, non-default (e.g. "en") at `/en/…`.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Current page language, e.g. "de" or "en". */
|
|
8
|
+
currentLang: string;
|
|
9
|
+
/** Current page pathname, e.g. "/" or "/en/about". */
|
|
10
|
+
pathname: string;
|
|
11
|
+
/** The default locale that uses bare paths (no prefix). Defaults to "de". */
|
|
12
|
+
defaultLocale?: string;
|
|
13
|
+
/** Explicit alternate URL for translated slugs (e.g. /en/privacy/ for /datenschutz/). Bypasses automatic derivation. */
|
|
14
|
+
alternateHref?: string;
|
|
15
|
+
/** Accessible label override. */
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { currentLang, pathname, defaultLocale = 'de', alternateHref: explicitHref, ariaLabel } = Astro.props;
|
|
20
|
+
|
|
21
|
+
const isDefault = currentLang === defaultLocale;
|
|
22
|
+
const alternateLang = isDefault ? 'en' : defaultLocale;
|
|
23
|
+
const label = alternateLang.toUpperCase();
|
|
24
|
+
|
|
25
|
+
function deriveAlternateHref(): string {
|
|
26
|
+
if (isDefault) {
|
|
27
|
+
return `/en${pathname === '/' ? '/' : pathname}`;
|
|
28
|
+
}
|
|
29
|
+
const stripped = pathname.replace(/^\/en\/?/, '/');
|
|
30
|
+
return stripped || '/';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolvedHref = explicitHref ?? deriveAlternateHref();
|
|
34
|
+
const resolvedAriaLabel = ariaLabel ?? `Switch to ${alternateLang}`;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<a href={resolvedHref} class="ew-lang-switch" aria-label={resolvedAriaLabel} hreflang={alternateLang}>
|
|
38
|
+
{label}
|
|
39
|
+
</a>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.ew-lang-switch {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
/* Same shared header-control height as ThemeToggle for a consistent utility row. */
|
|
48
|
+
height: 2.25rem;
|
|
49
|
+
min-width: 2.5rem;
|
|
50
|
+
padding: 0 var(--ew-space-2);
|
|
51
|
+
border: 1px solid var(--ew-border);
|
|
52
|
+
border-radius: var(--ew-radius-md);
|
|
53
|
+
color: var(--ew-on-surface);
|
|
54
|
+
font-family: var(--ew-font-sans);
|
|
55
|
+
font-size: var(--ew-text-sm);
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
letter-spacing: 0.04em;
|
|
59
|
+
}
|
|
60
|
+
.ew-lang-switch:hover {
|
|
61
|
+
background: var(--ew-surface-muted);
|
|
62
|
+
}
|
|
63
|
+
.ew-lang-switch:focus-visible {
|
|
64
|
+
outline: 2px solid var(--ew-primary);
|
|
65
|
+
outline-offset: 2px;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LegalLayout – narrow content wrapper for legal pages (privacy, imprint, etc.).
|
|
4
|
+
* Pure Astro component using --ew-* design tokens.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string;
|
|
8
|
+
lastUpdated?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title, lastUpdated } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<article class="ew-legal">
|
|
15
|
+
<div class="ew-legal__inner">
|
|
16
|
+
<h1 class="ew-legal__title">{title}</h1>
|
|
17
|
+
{lastUpdated && <p class="ew-legal__updated">{lastUpdated}</p>}
|
|
18
|
+
<div class="ew-legal__content">
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</article>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.ew-legal {
|
|
26
|
+
padding: var(--ew-space-8) var(--ew-space-5);
|
|
27
|
+
}
|
|
28
|
+
.ew-legal__inner {
|
|
29
|
+
max-width: 45rem;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
}
|
|
32
|
+
.ew-legal__title {
|
|
33
|
+
font-family: var(--ew-font-sans);
|
|
34
|
+
font-size: var(--ew-text-3xl);
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
color: var(--ew-on-surface);
|
|
37
|
+
margin: 0 0 var(--ew-space-4);
|
|
38
|
+
line-height: var(--ew-leading-tight);
|
|
39
|
+
}
|
|
40
|
+
.ew-legal__updated {
|
|
41
|
+
font-family: var(--ew-font-sans);
|
|
42
|
+
font-size: var(--ew-text-sm);
|
|
43
|
+
color: var(--ew-muted);
|
|
44
|
+
margin: 0 0 var(--ew-space-6);
|
|
45
|
+
line-height: var(--ew-leading-normal);
|
|
46
|
+
}
|
|
47
|
+
.ew-legal__content {
|
|
48
|
+
font-family: var(--ew-font-sans);
|
|
49
|
+
font-size: var(--ew-text-base);
|
|
50
|
+
color: var(--ew-on-surface);
|
|
51
|
+
line-height: var(--ew-leading-loose);
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* NotFound – shared brand-conform 404 content block.
|
|
4
|
+
*
|
|
5
|
+
* Slotted body component (no `<html>`/`<head>`/`<body>`): consumers wrap it in
|
|
6
|
+
* their own layout. Public by design (never auth-gated per ADR 0010), so no
|
|
7
|
+
* `client:*` directives, `<script>` tags, or auth imports.
|
|
8
|
+
*
|
|
9
|
+
* `heading` / `message` are CMS overrides; when absent, the inline LABELS
|
|
10
|
+
* dictionary supplies DE/EN fallback copy (`easy-web-i18n` intentionally does
|
|
11
|
+
* not ship a UI-string dictionary — inline LABELS is the source of truth for
|
|
12
|
+
* uncustomised sites).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Current page locale (e.g. `"de"`, `"en"`). */
|
|
17
|
+
currentLang: string;
|
|
18
|
+
/** Site default locale — used for `rootHref` auto-compute and LABELS fallback. */
|
|
19
|
+
defaultLocale: string;
|
|
20
|
+
/** Explicit back-home link. Auto-computed from `currentLang` vs `defaultLocale` when omitted. */
|
|
21
|
+
rootHref?: string;
|
|
22
|
+
/** Alternate locale URL. When present, renders a language switcher link. */
|
|
23
|
+
alternateHref?: string;
|
|
24
|
+
/** CMS override for the `<h1>` heading (falls back to LABELS). */
|
|
25
|
+
heading?: string;
|
|
26
|
+
/** CMS override for the paragraph message (falls back to LABELS). */
|
|
27
|
+
message?: string;
|
|
28
|
+
/** Optional illustration URL. When present, renders `<img>` above the heading. */
|
|
29
|
+
image?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LABELS: Record<
|
|
33
|
+
string,
|
|
34
|
+
{ backHome: string; notFoundHeading: string; notFoundMessage: string; switchLanguage: string }
|
|
35
|
+
> = {
|
|
36
|
+
de: {
|
|
37
|
+
backHome: 'Zur Startseite',
|
|
38
|
+
notFoundHeading: 'Seite nicht gefunden',
|
|
39
|
+
notFoundMessage: 'Die aufgerufene Seite existiert nicht.',
|
|
40
|
+
switchLanguage: 'English',
|
|
41
|
+
},
|
|
42
|
+
en: {
|
|
43
|
+
backHome: 'Home',
|
|
44
|
+
notFoundHeading: 'Page not found',
|
|
45
|
+
notFoundMessage: 'The requested page does not exist.',
|
|
46
|
+
switchLanguage: 'Deutsch',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const { currentLang, defaultLocale, rootHref, alternateHref, heading, message, image } = Astro.props;
|
|
51
|
+
|
|
52
|
+
const t = LABELS[currentLang] ?? LABELS[defaultLocale] ?? LABELS.en;
|
|
53
|
+
const resolvedRootHref = rootHref ?? (currentLang === defaultLocale ? '/' : `/${currentLang}/`);
|
|
54
|
+
const resolvedHeading = heading ?? t.notFoundHeading;
|
|
55
|
+
const resolvedMessage = message ?? t.notFoundMessage;
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<section class="ew-not-found" data-testid="not-found">
|
|
59
|
+
{image ? <img src={image} alt="" class="ew-not-found__image" loading="lazy" /> : null}
|
|
60
|
+
<h1 class="ew-not-found__heading">{resolvedHeading}</h1>
|
|
61
|
+
<p class="ew-not-found__message">{resolvedMessage}</p>
|
|
62
|
+
<p class="ew-not-found__actions">
|
|
63
|
+
<a href={resolvedRootHref} class="ew-not-found__link">{t.backHome}</a>
|
|
64
|
+
</p>
|
|
65
|
+
{alternateHref ? (
|
|
66
|
+
<p class="ew-not-found__alt">
|
|
67
|
+
<a href={alternateHref} class="ew-not-found__alt-link">{t.switchLanguage}</a>
|
|
68
|
+
</p>
|
|
69
|
+
) : null}
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
.ew-not-found {
|
|
74
|
+
max-width: 40rem;
|
|
75
|
+
margin: var(--ew-space-12) auto;
|
|
76
|
+
padding: 0 var(--ew-space-5);
|
|
77
|
+
text-align: center;
|
|
78
|
+
color: var(--ew-on-surface);
|
|
79
|
+
}
|
|
80
|
+
.ew-not-found__image {
|
|
81
|
+
display: block;
|
|
82
|
+
max-width: 12rem;
|
|
83
|
+
height: auto;
|
|
84
|
+
margin: 0 auto var(--ew-space-5);
|
|
85
|
+
}
|
|
86
|
+
.ew-not-found__heading {
|
|
87
|
+
font-family: var(--ew-font-sans);
|
|
88
|
+
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
|
89
|
+
color: var(--ew-on-surface);
|
|
90
|
+
margin: 0 0 var(--ew-space-3);
|
|
91
|
+
letter-spacing: -0.01em;
|
|
92
|
+
}
|
|
93
|
+
.ew-not-found__message {
|
|
94
|
+
color: var(--ew-on-surface);
|
|
95
|
+
margin: 0 0 var(--ew-space-5);
|
|
96
|
+
}
|
|
97
|
+
.ew-not-found__actions {
|
|
98
|
+
margin: 0 0 var(--ew-space-3);
|
|
99
|
+
}
|
|
100
|
+
.ew-not-found__link {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
padding: var(--ew-space-2) var(--ew-space-5);
|
|
103
|
+
background: var(--ew-primary);
|
|
104
|
+
color: var(--ew-on-primary);
|
|
105
|
+
border-radius: var(--ew-radius-md);
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
text-decoration: none;
|
|
108
|
+
}
|
|
109
|
+
.ew-not-found__link:hover {
|
|
110
|
+
filter: brightness(0.95);
|
|
111
|
+
}
|
|
112
|
+
.ew-not-found__link:focus-visible {
|
|
113
|
+
outline: 2px solid var(--ew-primary);
|
|
114
|
+
outline-offset: 2px;
|
|
115
|
+
}
|
|
116
|
+
.ew-not-found__alt {
|
|
117
|
+
margin: 0;
|
|
118
|
+
}
|
|
119
|
+
.ew-not-found__alt-link {
|
|
120
|
+
color: var(--ew-primary);
|
|
121
|
+
text-decoration: underline;
|
|
122
|
+
font-size: var(--ew-text-sm);
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
<div class="ew-prose">
|
|
5
|
+
<div class="ew-prose__inner">
|
|
6
|
+
<slot />
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<style>
|
|
11
|
+
.ew-prose {
|
|
12
|
+
padding: var(--ew-space-8) var(--ew-space-5);
|
|
13
|
+
}
|
|
14
|
+
.ew-prose__inner {
|
|
15
|
+
font-family: var(--ew-font-sans);
|
|
16
|
+
font-size: var(--ew-text-base);
|
|
17
|
+
line-height: var(--ew-leading-loose);
|
|
18
|
+
color: var(--ew-on-surface);
|
|
19
|
+
max-width: 45rem;
|
|
20
|
+
margin: 0 auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ew-prose__inner :global(h1) {
|
|
24
|
+
font-size: var(--ew-text-3xl);
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
line-height: var(--ew-leading-tight);
|
|
27
|
+
margin: var(--ew-space-7) 0 var(--ew-space-4);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ew-prose__inner :global(h2) {
|
|
31
|
+
font-size: var(--ew-text-xl);
|
|
32
|
+
font-weight: 700;
|
|
33
|
+
line-height: var(--ew-leading-tight);
|
|
34
|
+
margin: var(--ew-space-7) 0 var(--ew-space-3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ew-prose__inner :global(h3) {
|
|
38
|
+
font-size: var(--ew-text-lg);
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
line-height: var(--ew-leading-tight);
|
|
41
|
+
margin: var(--ew-space-6) 0 var(--ew-space-3);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ew-prose__inner :global(h4),
|
|
45
|
+
.ew-prose__inner :global(h5),
|
|
46
|
+
.ew-prose__inner :global(h6) {
|
|
47
|
+
font-size: var(--ew-text-base);
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
line-height: var(--ew-leading-tight);
|
|
50
|
+
margin: var(--ew-space-5) 0 var(--ew-space-2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ew-prose__inner :global(p) {
|
|
54
|
+
margin: 0 0 var(--ew-space-4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ew-prose__inner :global(a) {
|
|
58
|
+
color: var(--ew-primary);
|
|
59
|
+
text-decoration: underline;
|
|
60
|
+
text-underline-offset: 0.15em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ew-prose__inner :global(a:hover) {
|
|
64
|
+
text-decoration-thickness: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ew-prose__inner :global(ul),
|
|
68
|
+
.ew-prose__inner :global(ol) {
|
|
69
|
+
margin: 0 0 var(--ew-space-4);
|
|
70
|
+
padding-left: var(--ew-space-5);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ew-prose__inner :global(li) {
|
|
74
|
+
margin-bottom: var(--ew-space-1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ew-prose__inner :global(li > ul),
|
|
78
|
+
.ew-prose__inner :global(li > ol) {
|
|
79
|
+
margin-top: var(--ew-space-1);
|
|
80
|
+
margin-bottom: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ew-prose__inner :global(blockquote) {
|
|
84
|
+
border-left: 3px solid var(--ew-primary);
|
|
85
|
+
margin: 0 0 var(--ew-space-4);
|
|
86
|
+
padding: var(--ew-space-2) var(--ew-space-4);
|
|
87
|
+
color: var(--ew-muted);
|
|
88
|
+
font-style: italic;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ew-prose__inner :global(blockquote p:last-child) {
|
|
92
|
+
margin-bottom: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ew-prose__inner :global(code) {
|
|
96
|
+
font-family: var(--ew-font-mono);
|
|
97
|
+
font-size: 0.9em;
|
|
98
|
+
background: var(--ew-surface-muted);
|
|
99
|
+
border-radius: var(--ew-radius-sm);
|
|
100
|
+
padding: 0.15em 0.35em;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ew-prose__inner :global(pre) {
|
|
104
|
+
margin: 0 0 var(--ew-space-4);
|
|
105
|
+
padding: var(--ew-space-4);
|
|
106
|
+
background: var(--ew-surface-muted);
|
|
107
|
+
border-radius: var(--ew-radius-md);
|
|
108
|
+
overflow-x: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ew-prose__inner :global(pre code) {
|
|
112
|
+
background: none;
|
|
113
|
+
padding: 0;
|
|
114
|
+
border-radius: 0;
|
|
115
|
+
font-size: var(--ew-text-sm);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ew-prose__inner :global(hr) {
|
|
119
|
+
border: none;
|
|
120
|
+
border-top: 1px solid var(--ew-border);
|
|
121
|
+
margin: var(--ew-space-6) 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.ew-prose__inner :global(img) {
|
|
125
|
+
max-width: 100%;
|
|
126
|
+
height: auto;
|
|
127
|
+
border-radius: var(--ew-radius-md);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ew-prose__inner :global(table) {
|
|
131
|
+
width: 100%;
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
margin: 0 0 var(--ew-space-4);
|
|
134
|
+
font-size: var(--ew-text-sm);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ew-prose__inner :global(th),
|
|
138
|
+
.ew-prose__inner :global(td) {
|
|
139
|
+
border: 1px solid var(--ew-border);
|
|
140
|
+
padding: var(--ew-space-2) var(--ew-space-3);
|
|
141
|
+
text-align: left;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ew-prose__inner :global(th) {
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
background: var(--ew-surface-muted);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ew-prose__inner :global(:first-child) {
|
|
150
|
+
margin-top: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ew-prose__inner :global(:last-child) {
|
|
154
|
+
margin-bottom: 0;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Section – semantic wrapper with optional heading, constrained width, and slot.
|
|
4
|
+
* Pure Astro component using --ew-* design tokens.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
title?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title, id } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<section class="ew-section" id={id}>
|
|
15
|
+
<div class="ew-section__inner">
|
|
16
|
+
{title && <h2 class="ew-section__title">{title}</h2>}
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
19
|
+
</section>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.ew-section {
|
|
23
|
+
padding: var(--ew-space-8) var(--ew-space-5);
|
|
24
|
+
}
|
|
25
|
+
.ew-section__inner {
|
|
26
|
+
max-width: 72rem;
|
|
27
|
+
margin: 0 auto;
|
|
28
|
+
}
|
|
29
|
+
.ew-section__title {
|
|
30
|
+
font-family: var(--ew-font-sans);
|
|
31
|
+
font-size: var(--ew-text-xl);
|
|
32
|
+
font-weight: 700;
|
|
33
|
+
color: var(--ew-on-surface);
|
|
34
|
+
margin: 0 0 var(--ew-space-5);
|
|
35
|
+
line-height: var(--ew-leading-tight);
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ThemeToggle – switches between light and dark themes using @easy-web/theme-core.
|
|
4
|
+
* Pure Astro component with inline vanilla JS. No framework dependency.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Accessible label for the toggle button. */
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { label = 'Toggle color theme (Darkmode)' } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="ew-theme-toggle"
|
|
17
|
+
aria-label={label}
|
|
18
|
+
title={label}
|
|
19
|
+
>
|
|
20
|
+
<svg class="ew-theme-toggle__icon ew-theme-toggle__icon--sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
21
|
+
<circle cx="12" cy="12" r="5"></circle>
|
|
22
|
+
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
23
|
+
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
24
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
25
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
26
|
+
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
27
|
+
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
28
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
29
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
30
|
+
</svg>
|
|
31
|
+
<svg class="ew-theme-toggle__icon ew-theme-toggle__icon--moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
32
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.ew-theme-toggle {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
/* Shared header-control height. Icon-only → same value for width for a square button. */
|
|
43
|
+
width: 2.25rem;
|
|
44
|
+
height: 2.25rem;
|
|
45
|
+
background: transparent;
|
|
46
|
+
border: 1px solid var(--ew-border);
|
|
47
|
+
border-radius: var(--ew-radius-md);
|
|
48
|
+
color: var(--ew-on-surface);
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
padding: 0;
|
|
51
|
+
}
|
|
52
|
+
.ew-theme-toggle:hover {
|
|
53
|
+
background: var(--ew-surface-muted);
|
|
54
|
+
}
|
|
55
|
+
.ew-theme-toggle:focus-visible {
|
|
56
|
+
outline: 2px solid var(--ew-primary);
|
|
57
|
+
outline-offset: 2px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Default: show sun, hide moon (light mode) */
|
|
61
|
+
.ew-theme-toggle__icon--moon { display: none; }
|
|
62
|
+
.ew-theme-toggle__icon--sun { display: block; }
|
|
63
|
+
|
|
64
|
+
:global([data-theme="dark"]) .ew-theme-toggle__icon--sun { display: none; }
|
|
65
|
+
:global([data-theme="dark"]) .ew-theme-toggle__icon--moon { display: block; }
|
|
66
|
+
|
|
67
|
+
@media (prefers-color-scheme: dark) {
|
|
68
|
+
:global(:root:not([data-theme="light"])) .ew-theme-toggle__icon--sun { display: none; }
|
|
69
|
+
:global(:root:not([data-theme="light"])) .ew-theme-toggle__icon--moon { display: block; }
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
import { applyTheme, getPreferredTheme } from '@easy-web/theme-core';
|
|
75
|
+
|
|
76
|
+
const btn = document.querySelector('.ew-theme-toggle') as HTMLButtonElement | null;
|
|
77
|
+
btn?.addEventListener('click', () => {
|
|
78
|
+
const current = getPreferredTheme();
|
|
79
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
80
|
+
applyTheme(next);
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* UniversalMedia — renders an image from any source type.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Supported image formats: JPEG, PNG, WebP, AVIF, SVG, GIF.
|
|
7
|
+
* Unsupported formats (upload rejected at CMS level): HEIC, JXL.
|
|
8
|
+
*
|
|
9
|
+
* 4-case dispatch:
|
|
10
|
+
* 1. ImageMetadata object → <Image> (Astro-optimised, with srcset)
|
|
11
|
+
* 2. "/src/assets/..." string → glob lookup via `images` prop → <Image> (fallback to fallbackSrc)
|
|
12
|
+
* 3. External URL ("http..." or "https...") → <Image inferSize>
|
|
13
|
+
* 4. Everything else (public-folder path "/foo/bar.png") → plain <img>
|
|
14
|
+
*/
|
|
15
|
+
import { Image } from 'astro:assets';
|
|
16
|
+
import type { ImageMetadata } from 'astro';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
/** The image source: an Astro ImageMetadata object or a URL/path string. */
|
|
20
|
+
src: ImageMetadata | string;
|
|
21
|
+
/** Alt text for the image (required). */
|
|
22
|
+
alt: string;
|
|
23
|
+
/**
|
|
24
|
+
* Consumer-supplied glob result mapping paths to lazy image loaders.
|
|
25
|
+
* Consumer generates this via Vite's glob-import helper (see Astro/Vite docs)
|
|
26
|
+
* matching `/src/assets/**` with `{ import: 'default' }` so entries yield
|
|
27
|
+
* `() => Promise<{ default: ImageMetadata }>`.
|
|
28
|
+
* The component does NOT resolve the glob itself.
|
|
29
|
+
*/
|
|
30
|
+
images: Record<string, () => Promise<{ default: ImageMetadata }>>;
|
|
31
|
+
/** Fallback image path to use when a /src/assets/... glob lookup misses. Defaults to empty (renders nothing gracefully). */
|
|
32
|
+
fallbackSrc?: string;
|
|
33
|
+
/** Fallback alt text used when fallbackSrc is rendered. Defaults to alt. */
|
|
34
|
+
fallbackAlt?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
class?: string;
|
|
38
|
+
decoding?: 'async' | 'auto' | 'sync';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
src,
|
|
43
|
+
alt,
|
|
44
|
+
images,
|
|
45
|
+
fallbackSrc,
|
|
46
|
+
fallbackAlt,
|
|
47
|
+
width = 800,
|
|
48
|
+
height = 600,
|
|
49
|
+
class: cssClass = '',
|
|
50
|
+
decoding = 'async',
|
|
51
|
+
} = Astro.props;
|
|
52
|
+
|
|
53
|
+
// Case 1: already a resolved ImageMetadata object
|
|
54
|
+
const isImageMetadata = typeof src !== 'string';
|
|
55
|
+
|
|
56
|
+
// Normalise string to check against known patterns
|
|
57
|
+
const srcStr = isImageMetadata ? '' : (src as string);
|
|
58
|
+
|
|
59
|
+
// Case 2: /src/assets/... path — must be resolved via consumer-supplied glob
|
|
60
|
+
const isInternalSrcAssets = srcStr.startsWith('/src/assets/');
|
|
61
|
+
|
|
62
|
+
// Case 3: external URL
|
|
63
|
+
const isExternalUrl = srcStr.startsWith('http://') || srcStr.startsWith('https://');
|
|
64
|
+
|
|
65
|
+
// Resolve ImageMetadata for cases 1 & 2
|
|
66
|
+
let resolvedSrc: ImageMetadata | null = null;
|
|
67
|
+
let resolvedAlt = alt;
|
|
68
|
+
|
|
69
|
+
if (isImageMetadata) {
|
|
70
|
+
resolvedSrc = src as ImageMetadata;
|
|
71
|
+
} else if (isInternalSrcAssets) {
|
|
72
|
+
const loader = images[srcStr];
|
|
73
|
+
if (typeof loader === 'function') {
|
|
74
|
+
const mod = await loader();
|
|
75
|
+
resolvedSrc = mod.default ?? (mod as unknown as ImageMetadata);
|
|
76
|
+
}
|
|
77
|
+
if (!resolvedSrc && fallbackSrc) {
|
|
78
|
+
const fallbackLoader = images[fallbackSrc];
|
|
79
|
+
if (typeof fallbackLoader === 'function') {
|
|
80
|
+
const mod = await fallbackLoader();
|
|
81
|
+
resolvedSrc = mod.default ?? (mod as unknown as ImageMetadata);
|
|
82
|
+
resolvedAlt = fallbackAlt ?? alt;
|
|
83
|
+
} else {
|
|
84
|
+
console.warn(
|
|
85
|
+
`UniversalMedia: "${srcStr}" not found in glob, and fallbackSrc "${fallbackSrc}" is also missing. Rendering as plain <img>.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
{isImageMetadata || isInternalSrcAssets ? (
|
|
93
|
+
resolvedSrc ? (
|
|
94
|
+
<Image class={cssClass} src={resolvedSrc} alt={resolvedAlt} width={width} height={height} decoding={decoding} />
|
|
95
|
+
) : (
|
|
96
|
+
<img class={cssClass} src={srcStr} alt={alt} width={width} height={height} decoding={decoding} />
|
|
97
|
+
)
|
|
98
|
+
) : isExternalUrl ? (
|
|
99
|
+
<Image class={cssClass} src={srcStr} alt={alt} inferSize decoding={decoding} />
|
|
100
|
+
) : (
|
|
101
|
+
<img class={cssClass} src={srcStr} alt={alt} width={width} height={height} decoding={decoding} />
|
|
102
|
+
)}
|