@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
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=UniversalMedia.test.d.ts.map
@@ -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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notFound.test.d.ts.map
@@ -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"}