@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
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# @easy-web/content-blocks
|
|
2
|
+
|
|
3
|
+
Reusable Astro content-block components for the IT-CI ismaili.de web ecosystem. Twenty-two pure-Astro components covering page chrome (Header, Footer, ThemeToggle, LanguageSwitch), hero/CTA/contact sections, cards and grids, a CMS-driven gallery system with six variants, blog post cards, legal-page layouts, banners, and styled prose. All components use the `--ew-*` design tokens from `@easy-web/theme-core`; no styling system of their own.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @easy-web/content-blocks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This package is published to npm. Install it like any other npm package.
|
|
12
|
+
|
|
13
|
+
### Peer dependencies
|
|
14
|
+
|
|
15
|
+
* `astro >= 6.0.0`
|
|
16
|
+
|
|
17
|
+
You almost certainly also want `@easy-web/theme-core` (for the `--ew-*` token CSS) and, if you build a bilingual site, `@easy-web/i18n` (for the alternate-link helper used by `LanguageSwitch`).
|
|
18
|
+
|
|
19
|
+
## Import convention
|
|
20
|
+
|
|
21
|
+
Components ship as **raw `.astro` files**. There is no JavaScript barrel. Import each component from its component path:
|
|
22
|
+
|
|
23
|
+
```astro
|
|
24
|
+
---
|
|
25
|
+
import Header from '@easy-web/content-blocks/components/Header';
|
|
26
|
+
import Footer from '@easy-web/content-blocks/components/Footer';
|
|
27
|
+
import Hero from '@easy-web/content-blocks/components/Hero';
|
|
28
|
+
import Section from '@easy-web/content-blocks/components/Section';
|
|
29
|
+
import CardGrid from '@easy-web/content-blocks/components/CardGrid';
|
|
30
|
+
import Card from '@easy-web/content-blocks/components/Card';
|
|
31
|
+
---
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The package's `package.json` exports map declares `./components/*` → `./src/components/*.astro`, so Astro consumes the source files directly — no build step is involved on the package side for the components themselves.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
### Layout — wire Header and Footer into your base layout
|
|
39
|
+
|
|
40
|
+
```astro
|
|
41
|
+
---
|
|
42
|
+
// src/layouts/Base.astro
|
|
43
|
+
import '@easy-web/theme-core/tokens.css';
|
|
44
|
+
import { noFlashScript } from '@easy-web/theme-core';
|
|
45
|
+
import Header from '@easy-web/content-blocks/components/Header';
|
|
46
|
+
import Footer from '@easy-web/content-blocks/components/Footer';
|
|
47
|
+
import { siteConfig } from '../config';
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
lang: string;
|
|
51
|
+
title: string;
|
|
52
|
+
pathname: string;
|
|
53
|
+
alternateHref?: string;
|
|
54
|
+
}
|
|
55
|
+
const { lang, title, pathname, alternateHref } = Astro.props;
|
|
56
|
+
const navItems = siteConfig.navItems[lang];
|
|
57
|
+
const legalLinks = siteConfig.legalLinks[lang];
|
|
58
|
+
---
|
|
59
|
+
<html lang={lang}>
|
|
60
|
+
<head>
|
|
61
|
+
<script is:inline set:html={noFlashScript}></script>
|
|
62
|
+
<title>{title}</title>
|
|
63
|
+
</head>
|
|
64
|
+
<body>
|
|
65
|
+
<Header
|
|
66
|
+
siteName={siteConfig.siteName}
|
|
67
|
+
navItems={navItems}
|
|
68
|
+
currentLang={lang}
|
|
69
|
+
pathname={pathname}
|
|
70
|
+
alternateHref={alternateHref}
|
|
71
|
+
/>
|
|
72
|
+
<slot />
|
|
73
|
+
<Footer siteName={siteConfig.siteName} legalLinks={legalLinks} />
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Page — compose Hero + Section + CardGrid + Card
|
|
79
|
+
|
|
80
|
+
```astro
|
|
81
|
+
---
|
|
82
|
+
// src/pages/index.astro
|
|
83
|
+
import Layout from '../layouts/Base.astro';
|
|
84
|
+
import Hero from '@easy-web/content-blocks/components/Hero';
|
|
85
|
+
import Section from '@easy-web/content-blocks/components/Section';
|
|
86
|
+
import CardGrid from '@easy-web/content-blocks/components/CardGrid';
|
|
87
|
+
import Card from '@easy-web/content-blocks/components/Card';
|
|
88
|
+
---
|
|
89
|
+
<Layout lang="de" title="Home" pathname="/">
|
|
90
|
+
<Hero
|
|
91
|
+
title="Willkommen"
|
|
92
|
+
subtitle="Untertitel mit Beschreibung"
|
|
93
|
+
ctaLabel="Mehr erfahren"
|
|
94
|
+
ctaHref="#about"
|
|
95
|
+
/>
|
|
96
|
+
<Section id="about">
|
|
97
|
+
<h2>Über uns</h2>
|
|
98
|
+
<p>Inhalt …</p>
|
|
99
|
+
</Section>
|
|
100
|
+
<Section>
|
|
101
|
+
<CardGrid>
|
|
102
|
+
<Card title="Erste Karte" description="Kurze Beschreibung" href="/a" />
|
|
103
|
+
<Card title="Zweite Karte" description="Kurze Beschreibung" href="/b" />
|
|
104
|
+
<Card title="Dritte Karte" description="Kurze Beschreibung" href="/c" />
|
|
105
|
+
</CardGrid>
|
|
106
|
+
</Section>
|
|
107
|
+
</Layout>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Component reference
|
|
111
|
+
|
|
112
|
+
All components use the `--ew-*` design tokens from `@easy-web/theme-core`. None ship their own theme system; they inherit whatever tokens are loaded in the consuming page.
|
|
113
|
+
|
|
114
|
+
### Site chrome
|
|
115
|
+
|
|
116
|
+
| Component | Required props | Optional props | Notes |
|
|
117
|
+
| :--- | :--- | :--- | :--- |
|
|
118
|
+
| `Header` | `siteName: string`, `navItems: NavItem[]`, `currentLang: string`, `pathname: string` | `alternateHref?: string`, `menuLabel?: string` | Sticky responsive header with `actions` slot. Mobile hamburger menu (scoped vanilla JS). Default slot fallback renders `ThemeToggle` + `LanguageSwitch`. `data-testid="header-classic"`. |
|
|
119
|
+
| `HeaderCentered` | `siteName: string`, `navItems: NavItem[]`, `currentLang: string`, `pathname: string` | `alternateHref?: string`, `menuLabel?: string` | Variant: brand centered, nav links split left/right. Same `actions` slot and mobile hamburger. `data-testid="header-centered"`. |
|
|
120
|
+
| `HeaderHideOnScroll` | `siteName: string`, `navItems: NavItem[]`, `currentLang: string`, `pathname: string` | `alternateHref?: string`, `menuLabel?: string` | Variant: `position: fixed` header that slides off screen on scroll-down, reappears on scroll-up. Compensates body padding via JS. `data-testid="header-hide-on-scroll"`. |
|
|
121
|
+
| `HeaderFlyout` | `siteName: string`, `navItems: NavItem[]`, `currentLang: string`, `pathname: string` | `alternateHref?: string`, `menuLabel?: string` | Variant: items with `children[]` show a flyout dropdown (hover/focus desktop, click mobile, keyboard accessible). `data-testid="header-flyout"`. |
|
|
122
|
+
| `Footer` | `siteName: string`, `legalLinks: Array<{ label: string; href: string }>` | — | Simple copyright + legal-link footer. |
|
|
123
|
+
| `ThemeToggle` | — | — | Light / dark / system trio button. Talks to `@easy-web/theme-core` via `data-theme` on `<html>` and `localStorage`. |
|
|
124
|
+
| `LanguageSwitch` | `currentLang: string`, `pathname: string` | `alternateHref?: string` | DE ↔ EN switcher. If `alternateHref` is provided, links there directly; otherwise infers the alternate path from `pathname`. |
|
|
125
|
+
|
|
126
|
+
#### Header `actions` slot
|
|
127
|
+
|
|
128
|
+
All four header variants expose a named `actions` slot. When the slot receives content, it replaces the default `ThemeToggle` + `LanguageSwitch` controls. When the slot is empty, the fallback renders those controls unchanged (backward compatible).
|
|
129
|
+
|
|
130
|
+
```astro
|
|
131
|
+
---
|
|
132
|
+
import Header from '@easy-web/content-blocks/components/Header';
|
|
133
|
+
import HeaderCentered from '@easy-web/content-blocks/components/HeaderCentered';
|
|
134
|
+
import HeaderHideOnScroll from '@easy-web/content-blocks/components/HeaderHideOnScroll';
|
|
135
|
+
import HeaderFlyout from '@easy-web/content-blocks/components/HeaderFlyout';
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
<!-- Default: ThemeToggle + LanguageSwitch rendered automatically -->
|
|
139
|
+
<Header siteName="My Site" navItems={navItems} currentLang="de" pathname={pathname} />
|
|
140
|
+
|
|
141
|
+
<!-- Custom slot content replaces ThemeToggle + LanguageSwitch -->
|
|
142
|
+
<HeaderCentered siteName="My Site" navItems={navItems} currentLang="de" pathname={pathname}>
|
|
143
|
+
<fragment slot="actions">
|
|
144
|
+
<MyCustomButton />
|
|
145
|
+
</fragment>
|
|
146
|
+
</HeaderCentered>
|
|
147
|
+
|
|
148
|
+
<!-- Flyout: navItems may include children[] for dropdown panels -->
|
|
149
|
+
<HeaderFlyout siteName="My Site" navItems={navItemsWithChildren} currentLang="de" pathname={pathname} />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Selecting the header variant in a Base layout via a `headerVariant` prop:
|
|
153
|
+
|
|
154
|
+
```astro
|
|
155
|
+
---
|
|
156
|
+
// Base.astro
|
|
157
|
+
import Header from '@easy-web/content-blocks/components/Header';
|
|
158
|
+
import HeaderCentered from '@easy-web/content-blocks/components/HeaderCentered';
|
|
159
|
+
import HeaderHideOnScroll from '@easy-web/content-blocks/components/HeaderHideOnScroll';
|
|
160
|
+
import HeaderFlyout from '@easy-web/content-blocks/components/HeaderFlyout';
|
|
161
|
+
|
|
162
|
+
interface Props {
|
|
163
|
+
headerVariant?: 'classic' | 'centered' | 'hide-on-scroll' | 'flyout';
|
|
164
|
+
// …other props
|
|
165
|
+
}
|
|
166
|
+
const { headerVariant = 'classic' } = Astro.props;
|
|
167
|
+
const HeaderComponents = { classic: Header, centered: HeaderCentered, 'hide-on-scroll': HeaderHideOnScroll, flyout: HeaderFlyout };
|
|
168
|
+
const ActiveHeader = HeaderComponents[headerVariant];
|
|
169
|
+
---
|
|
170
|
+
<ActiveHeader siteName={siteName} navItems={navItems} currentLang={lang} pathname={pathname} alternateHref={alternateHref} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Hero / call-to-action / contact
|
|
174
|
+
|
|
175
|
+
| Component | Required props | Optional props | Notes |
|
|
176
|
+
| :--- | :--- | :--- | :--- |
|
|
177
|
+
| `Hero` | `title: string` | `subtitle?: string`, `ctaLabel?: string`, `ctaHref?: string`, `variant?: 'centered' \| 'left-aligned'` | Page banner with optional CTA. CTA renders only when both `ctaLabel` and `ctaHref` are present. |
|
|
178
|
+
| `CtaSection` | `heading: string`, `buttonLabel: string`, `buttonHref: string` | `body?: string`, `variant?: 'default' \| 'muted' \| 'primary'` | Standalone CTA banner. `primary` variant inverts colors to brand. |
|
|
179
|
+
| `ContactSection` | `heading: string`, `email: string`, `buttonLabel: string` | `body?: string` | Centered mailto-only contact CTA. Button is `mailto:${email}`. |
|
|
180
|
+
|
|
181
|
+
### Structural
|
|
182
|
+
|
|
183
|
+
| Component | Required props | Optional props | Notes |
|
|
184
|
+
| :--- | :--- | :--- | :--- |
|
|
185
|
+
| `Section` | — | `id?: string`, `class?: string` | Generic content wrapper. `id` enables anchor-link targets. Slot-based. |
|
|
186
|
+
| `CardGrid` | — | — | Responsive CSS grid (3 → 2 → 1 columns by breakpoint). Slot in `Card`s. |
|
|
187
|
+
| `Card` | `title: string` | `description?: string`, `href?: string`, `image?: string`, `imageAlt?: string` | Content card. Renders as `<a>` when `href` is provided, plain `<div>` otherwise. Image is `loading="lazy"`. |
|
|
188
|
+
|
|
189
|
+
### Gallery system
|
|
190
|
+
|
|
191
|
+
The gallery system is **CMS-driven**: content authors create gallery entries in their site's `src/content/galleries/` directory; pages decide which entry to load and where to place it. Each entry has a `kind` field that selects the rendering variant.
|
|
192
|
+
|
|
193
|
+
Six variants are available:
|
|
194
|
+
|
|
195
|
+
| `kind` | Component | Use case |
|
|
196
|
+
| :--- | :--- | :--- |
|
|
197
|
+
| `image-grid` | `GalleryImageGrid` | Static N-column uniform grid. Component previews, team photos, sponsors. |
|
|
198
|
+
| `hero-slider` | `GalleryHeroSlider` | Full-bleed editorial carousel with title + subtitle + optional CTA per slide. |
|
|
199
|
+
| `carousel` | `GalleryCarousel` | Image slider with optional captions. Constrained-width container; no per-slide CTA. |
|
|
200
|
+
| `masonry-grid` | `GalleryMasonryGrid` | Pinterest-style variable-height grid via CSS columns. Photography, portfolios. |
|
|
201
|
+
| `feature-highlight` | `GalleryFeatureHighlight` | Alternating image+text+link rows. Product feature showcase, service descriptions. |
|
|
202
|
+
| `lightbox-grid` | `GalleryLightboxGrid` | Uniform grid + click-to-expand modal. Photo albums, event galleries. Vanilla JS modal with keyboard navigation. |
|
|
203
|
+
|
|
204
|
+
Schema is exported from this package and consumed by each instance's `content.config.ts`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { defineCollection } from 'astro:content';
|
|
208
|
+
import { glob } from 'astro/loaders';
|
|
209
|
+
import { gallerySchema } from '@easy-web/content-blocks/schemas/galleries';
|
|
210
|
+
|
|
211
|
+
const galleries = defineCollection({
|
|
212
|
+
loader: glob({ pattern: '**/*.{yml,yaml}', base: './src/content/galleries' }),
|
|
213
|
+
schema: gallerySchema,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Use the `GallerySection` dispatcher in pages — it switches on `entry.data.kind` and renders the right variant:
|
|
218
|
+
|
|
219
|
+
```astro
|
|
220
|
+
---
|
|
221
|
+
import GallerySection from '@easy-web/content-blocks/components/GallerySection';
|
|
222
|
+
import { getEntry } from 'astro:content';
|
|
223
|
+
|
|
224
|
+
const heroEntry = await getEntry('galleries', 'home-hero-de');
|
|
225
|
+
const previewEntry = await getEntry('galleries', 'about-component-preview-de');
|
|
226
|
+
---
|
|
227
|
+
<GallerySection entry={heroEntry} />
|
|
228
|
+
<!-- ...other page content... -->
|
|
229
|
+
<GallerySection entry={previewEntry} />
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Component | Required props | Optional props | Notes |
|
|
233
|
+
| :--- | :--- | :--- | :--- |
|
|
234
|
+
| `GallerySection` | `entry: { data: GalleryEntry }` | — | Dispatcher. Renders the correct variant based on `entry.data.kind`. Renders nothing when `entry` is null/undefined. |
|
|
235
|
+
| `GalleryImageGrid` | `items: GalleryImageGridData['items']` | `title?`, `columns?: 2 \| 3 \| 4 \| 6`, `gap?: 'sm' \| 'md' \| 'lg'`, `aspectRatio?: 'square' \| '4/3' \| '16/9' \| 'auto'` | Pure CSS grid (responsive). Item `href` wraps in `<a>`. Item `caption` renders as `<figcaption>`. |
|
|
236
|
+
| `GalleryHeroSlider` | `slides: GalleryHeroSliderData['slides']` | `title?`, `autoplay?: boolean`, `interval?: number` (ms), `height?: 'sm' \| 'md' \| 'lg' \| 'full'` | Vanilla JS slider. Pause on hover/focus, keyboard arrows, dot navigation, respects `prefers-reduced-motion`. |
|
|
237
|
+
| `GalleryCarousel` | `slides: GalleryCarouselData['slides']` | `title?`, `autoplay?: boolean` (default `false`), `interval?: number`, `showDots?: boolean`, `showArrows?: boolean` | Constrained-width slider with optional per-slide caption (overlay at bottom). Same JS controls as `GalleryHeroSlider`. |
|
|
238
|
+
| `GalleryMasonryGrid` | `items: GalleryMasonryGridData['items']` | `title?`, `columns?: 2 \| 3 \| 4`, `gap?: 'sm' \| 'md' \| 'lg'` | Pure CSS columns; no JS. `break-inside: avoid` keeps items intact. Falls back to fewer columns on smaller breakpoints. |
|
|
239
|
+
| `GalleryFeatureHighlight` | `items: GalleryFeatureHighlightData['items']` | `title?`, `gap?: 'sm' \| 'md' \| 'lg'` | Alternating image-left / image-right rows; per-item `imagePosition` overrides the alternation. Stacks on mobile (image first). |
|
|
240
|
+
| `GalleryLightboxGrid` | `items: GalleryLightboxGridData['items']` | `title?`, `columns?: 2 \| 3 \| 4`, `gap?: 'sm' \| 'md' \| 'lg'`, `aspectRatio?: 'square' \| '4/3' \| 'auto'` | Grid + modal lightbox. Click image to expand; keyboard `Esc`/arrows navigate; backdrop click closes; restores focus to trigger on close. Body scroll locked while open. |
|
|
241
|
+
|
|
242
|
+
### Blog
|
|
243
|
+
|
|
244
|
+
| Component | Required props | Optional props | Notes |
|
|
245
|
+
| :--- | :--- | :--- | :--- |
|
|
246
|
+
| `BlogPostCard` | `title: string`, `description: string`, `href: string`, `pubDate: Date` | `heroImage?: string`, `heroImageAlt?: string`, `locale?: string` | Linked card for blog index pages. Date is `Intl.DateTimeFormat`-formatted with the provided `locale`. |
|
|
247
|
+
|
|
248
|
+
### Legal & long-form
|
|
249
|
+
|
|
250
|
+
| Component | Required props | Optional props | Notes |
|
|
251
|
+
| :--- | :--- | :--- | :--- |
|
|
252
|
+
| `LegalLayout` | — | (slot-based) | Narrow content wrapper for impressum / datenschutz / AGB. Constrains line-length for legal prose. |
|
|
253
|
+
| `Prose` | — | (slot-based) | Styled wrapper for Markdown/HTML content (`<h1>`–`<h6>`, `<p>`, `<ul>`, `<a>`, `<blockquote>`, `<code>`, etc.) using `--ew-*` typography tokens. |
|
|
254
|
+
|
|
255
|
+
### Banners / notices
|
|
256
|
+
|
|
257
|
+
| Component | Required props | Optional props | Notes |
|
|
258
|
+
| :--- | :--- | :--- | :--- |
|
|
259
|
+
| `DraftBanner` | — | (slot-based, expects a short message) | Banner notice for draft content. |
|
|
260
|
+
| `LanguageNotice` | — | (slot-based, expects a translation-status message) | Banner notice when a page is shown in the non-current locale or has a partial translation. |
|
|
261
|
+
|
|
262
|
+
## <UniversalMedia>
|
|
263
|
+
|
|
264
|
+
A universal image dispatcher component that renders images from any source type consistently.
|
|
265
|
+
|
|
266
|
+
### Props
|
|
267
|
+
|
|
268
|
+
| Prop | Type | Required | Description |
|
|
269
|
+
|------|------|----------|-------------|
|
|
270
|
+
| `src` | `ImageMetadata \| string` | ✓ | Image source: resolved metadata or a URL/path string |
|
|
271
|
+
| `alt` | `string` | ✓ | Alt text |
|
|
272
|
+
| `images` | `Record<string, () => Promise<{ default: ImageMetadata }>>` | ✓ | Consumer-supplied glob result (pass `import.meta.glob(...)` from the calling component) |
|
|
273
|
+
| `fallbackSrc` | `string` | — | Fallback image path when `/src/assets/` glob lookup fails |
|
|
274
|
+
| `fallbackAlt` | `string` | — | Fallback alt text (defaults to `alt`) |
|
|
275
|
+
| `width` | `number` | — | Image width in px (default: 800) |
|
|
276
|
+
| `height` | `number` | — | Image height in px (default: 600) |
|
|
277
|
+
| `class` | `string` | — | CSS class name |
|
|
278
|
+
| `decoding` | `'async' \| 'auto' \| 'sync'` | — | Image decoding hint (default: `'async'`) |
|
|
279
|
+
|
|
280
|
+
### Dispatch logic
|
|
281
|
+
|
|
282
|
+
1. **`ImageMetadata` object** → `<Image>` (Astro-optimised, generates srcset)
|
|
283
|
+
2. **`/src/assets/...` string** → glob lookup via `images` prop → `<Image>` (or fallback/plain `<img>` on miss)
|
|
284
|
+
3. **`http://...` / `https://...` string** → `<Image inferSize>` (reads dimensions from remote)
|
|
285
|
+
4. **Everything else** (e.g., `/public/...` paths) → plain `<img>`
|
|
286
|
+
|
|
287
|
+
> **Unsupported formats**: HEIC and JXL. Reject these at the CMS upload level (e.g., Decap CMS widget `hint`).
|
|
288
|
+
|
|
289
|
+
### Usage
|
|
290
|
+
|
|
291
|
+
```astro
|
|
292
|
+
---
|
|
293
|
+
import type { ImageMetadata } from 'astro';
|
|
294
|
+
import UniversalMedia from '@easy-web/content-blocks/components/UniversalMedia';
|
|
295
|
+
|
|
296
|
+
const images = import.meta.glob<{ default: ImageMetadata }>('/src/assets/**', { import: 'default' });
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
<UniversalMedia
|
|
300
|
+
src={entry.data.photo}
|
|
301
|
+
alt={entry.data.photoAlt}
|
|
302
|
+
{images}
|
|
303
|
+
fallbackSrc="/src/assets/images/default.png"
|
|
304
|
+
fallbackAlt="Default image"
|
|
305
|
+
/>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Styling
|
|
309
|
+
|
|
310
|
+
All components reference the `@easy-web/theme-core` CSS custom properties:
|
|
311
|
+
|
|
312
|
+
* Colors: `--ew-surface`, `--ew-on-surface`, `--ew-surface-muted`, `--ew-primary`, `--ew-on-primary`, `--ew-border`, `--ew-muted`
|
|
313
|
+
* Typography: `--ew-font-sans`, `--ew-text-{xs,sm,base,lg,xl,2xl,3xl}`, `--ew-leading-{tight,normal,loose}`
|
|
314
|
+
* Spacing: `--ew-space-{0..12}`
|
|
315
|
+
* Radii: `--ew-radius-{sm,md,lg,xl,full}`
|
|
316
|
+
|
|
317
|
+
To re-skin a site, override these custom properties on a parent element (or globally on `:root`) — do not edit the components.
|
|
318
|
+
|
|
319
|
+
Components use BEM-style class names prefixed with `ew-` (e.g., `.ew-hero`, `.ew-hero__title`, `.ew-hero--centered`). Selectors are intentionally specific enough to be styled via standard CSS overrides without `!important`.
|
|
320
|
+
|
|
321
|
+
## Adoption status
|
|
322
|
+
|
|
323
|
+
The [package adoption matrix](https://github.com/achimismaili/websites/blob/main/docs/architecture/package-adoption.md) tracks which sites consume which version of this package. As of 2026-06-10:
|
|
324
|
+
|
|
325
|
+
* `dev.ismaili.de` (pilot) — `^0.2.0`
|
|
326
|
+
* `harleyrentflorida.de` (customer) — `^0.1.0` (one minor behind; bump pending while `/preview` is paused)
|
|
327
|
+
|
|
328
|
+
## See also
|
|
329
|
+
|
|
330
|
+
* [`@easy-web/theme-core`](https://github.com/achimismaili/easy-web/blob/main/packages/theme-core/README.md) — the design-token package these components consume.
|
|
331
|
+
* [`@easy-web/i18n`](https://github.com/achimismaili/easy-web/blob/main/packages/i18n/README.md) — Astro + Paraglide bilingual utilities used together with `LanguageSwitch`.
|
|
332
|
+
* [`easy-web/AGENTS.md`](https://github.com/achimismaili/easy-web/blob/main/AGENTS.md) — repo orientation and publishing workflow.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniversalMedia.test.d.ts","sourceRoot":"","sources":["../../../src/components/__tests__/UniversalMedia.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
function classifySource(src) {
|
|
3
|
+
if (typeof src !== 'string')
|
|
4
|
+
return 'image-metadata';
|
|
5
|
+
if (src.startsWith('/src/assets/'))
|
|
6
|
+
return 'internal-src-assets';
|
|
7
|
+
if (src.startsWith('http://') || src.startsWith('https://'))
|
|
8
|
+
return 'external-url';
|
|
9
|
+
return 'public-path';
|
|
10
|
+
}
|
|
11
|
+
// Minimal ImageMetadata stub for test case 1
|
|
12
|
+
const mockImageMetadata = {
|
|
13
|
+
src: '/src/assets/images/test.png',
|
|
14
|
+
width: 100,
|
|
15
|
+
height: 100,
|
|
16
|
+
format: 'png',
|
|
17
|
+
};
|
|
18
|
+
describe('<UniversalMedia> dispatch logic', () => {
|
|
19
|
+
it('case 1: ImageMetadata object → image-metadata', () => {
|
|
20
|
+
expect(classifySource(mockImageMetadata)).toBe('image-metadata');
|
|
21
|
+
});
|
|
22
|
+
it('case 2: /src/assets/ string → internal-src-assets (glob lookup path)', () => {
|
|
23
|
+
expect(classifySource('/src/assets/images/photo.jpg')).toBe('internal-src-assets');
|
|
24
|
+
});
|
|
25
|
+
it('case 3: https:// URL → external-url (inferSize path)', () => {
|
|
26
|
+
expect(classifySource('https://example.com/photo.jpg')).toBe('external-url');
|
|
27
|
+
});
|
|
28
|
+
it('case 4: public path /foo/bar.png → public-path (plain img)', () => {
|
|
29
|
+
expect(classifySource('/uploads/photo.jpg')).toBe('public-path');
|
|
30
|
+
});
|
|
31
|
+
it('case 5: glob miss + fallbackSrc → fallback branch applies when resolvedSrc is null', () => {
|
|
32
|
+
// Simulate glob miss: empty images map for a /src/assets/ URL
|
|
33
|
+
const images = {};
|
|
34
|
+
const src = '/src/assets/images/missing.png';
|
|
35
|
+
const fallbackSrc = '/src/assets/images/default.png';
|
|
36
|
+
const dispatch = classifySource(src);
|
|
37
|
+
expect(dispatch).toBe('internal-src-assets');
|
|
38
|
+
// Verify fallback key lookup works
|
|
39
|
+
const hasMainImage = src in images;
|
|
40
|
+
const hasFallback = fallbackSrc in images;
|
|
41
|
+
expect(hasMainImage).toBe(false); // glob miss
|
|
42
|
+
expect(hasFallback).toBe(false); // fallback also missing → plain img fallback
|
|
43
|
+
// The component would render plain <img> when both are missing (documented in UniversalMedia)
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=UniversalMedia.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UniversalMedia.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/UniversalMedia.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAe9C,SAAS,cAAc,CAAC,GAA2B;IACjD,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC;IACrD,IAAI,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC;QAAE,OAAO,qBAAqB,CAAC;IACjE,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,cAAc,CAAC;IACnF,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,6CAA6C;AAC7C,MAAM,iBAAiB,GAAkB;IACvC,GAAG,EAAE,6BAA6B;IAClC,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,KAAK;CACd,CAAC;AAEF,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,cAAc,CAAC,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,cAAc,CAAC,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE,GAAG,EAAE;QAC5F,8DAA8D;QAC9D,MAAM,MAAM,GAA8D,EAAE,CAAC;QAC7E,MAAM,GAAG,GAAG,gCAAgC,CAAC;QAC7C,MAAM,WAAW,GAAG,gCAAgC,CAAC;QAErD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAE7C,mCAAmC;QACnC,MAAM,YAAY,GAAG,GAAG,IAAI,MAAM,CAAC;QACnC,MAAM,WAAW,GAAG,WAAW,IAAI,MAAM,CAAC;QAC1C,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY;QAC9C,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,6CAA6C;QAE9E,8FAA8F;IAChG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notFound.test.d.ts","sourceRoot":"","sources":["../../../src/schemas/__tests__/notFound.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { notFoundSchema } from '../notFound.js';
|
|
3
|
+
describe('notFoundSchema', () => {
|
|
4
|
+
it('parses a multi-locale object with partial fields', () => {
|
|
5
|
+
const input = {
|
|
6
|
+
de: { heading: 'Seite nicht gefunden', message: 'Existiert nicht.' },
|
|
7
|
+
en: { heading: 'Not found' },
|
|
8
|
+
};
|
|
9
|
+
expect(() => notFoundSchema.parse(input)).not.toThrow();
|
|
10
|
+
const result = notFoundSchema.parse(input);
|
|
11
|
+
expect(result.de.heading).toBe('Seite nicht gefunden');
|
|
12
|
+
expect(result.en.heading).toBe('Not found');
|
|
13
|
+
});
|
|
14
|
+
it('parses an empty object (all fields optional)', () => {
|
|
15
|
+
expect(() => notFoundSchema.parse({})).not.toThrow();
|
|
16
|
+
expect(notFoundSchema.parse({})).toEqual({});
|
|
17
|
+
});
|
|
18
|
+
it('parses arbitrary locale keys (open record)', () => {
|
|
19
|
+
const input = { de: {}, fr: {}, sq: {} };
|
|
20
|
+
expect(() => notFoundSchema.parse(input)).not.toThrow();
|
|
21
|
+
const result = notFoundSchema.parse(input);
|
|
22
|
+
expect(Object.keys(result)).toEqual(expect.arrayContaining(['de', 'fr', 'sq']));
|
|
23
|
+
});
|
|
24
|
+
it('rejects a record where image is not a string', () => {
|
|
25
|
+
const input = { de: { image: 42 } };
|
|
26
|
+
expect(() => notFoundSchema.parse(input)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=notFound.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notFound.test.js","sourceRoot":"","sources":["../../../src/schemas/__tests__/notFound.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,KAAK,GAAG;YACZ,EAAE,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,kBAAkB,EAAE;YACpE,EAAE,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;SAC7B,CAAC;QACF,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACrD,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,KAAK,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,KAAK,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|