@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,65 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { ImageMetadata } from 'astro';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors the dispatch logic inside UniversalMedia.astro.
|
|
6
|
+
* Tests the categorisation, not the Astro rendering (which requires a full Astro setup).
|
|
7
|
+
*
|
|
8
|
+
* The component branches on 4 cases:
|
|
9
|
+
* 1. ImageMetadata object → <Image> (Astro-optimised)
|
|
10
|
+
* 2. "/src/assets/..." string → glob lookup → <Image> (with fallback path)
|
|
11
|
+
* 3. "http(s)://..." string → <Image inferSize>
|
|
12
|
+
* 4. Everything else (public path) → plain <img>
|
|
13
|
+
*/
|
|
14
|
+
type DispatchCase = 'image-metadata' | 'internal-src-assets' | 'external-url' | 'public-path';
|
|
15
|
+
|
|
16
|
+
function classifySource(src: ImageMetadata | string): DispatchCase {
|
|
17
|
+
if (typeof src !== 'string') return 'image-metadata';
|
|
18
|
+
if (src.startsWith('/src/assets/')) return 'internal-src-assets';
|
|
19
|
+
if (src.startsWith('http://') || src.startsWith('https://')) return 'external-url';
|
|
20
|
+
return 'public-path';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Minimal ImageMetadata stub for test case 1
|
|
24
|
+
const mockImageMetadata: ImageMetadata = {
|
|
25
|
+
src: '/src/assets/images/test.png',
|
|
26
|
+
width: 100,
|
|
27
|
+
height: 100,
|
|
28
|
+
format: 'png',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('<UniversalMedia> dispatch logic', () => {
|
|
32
|
+
it('case 1: ImageMetadata object → image-metadata', () => {
|
|
33
|
+
expect(classifySource(mockImageMetadata)).toBe('image-metadata');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('case 2: /src/assets/ string → internal-src-assets (glob lookup path)', () => {
|
|
37
|
+
expect(classifySource('/src/assets/images/photo.jpg')).toBe('internal-src-assets');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('case 3: https:// URL → external-url (inferSize path)', () => {
|
|
41
|
+
expect(classifySource('https://example.com/photo.jpg')).toBe('external-url');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('case 4: public path /foo/bar.png → public-path (plain img)', () => {
|
|
45
|
+
expect(classifySource('/uploads/photo.jpg')).toBe('public-path');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('case 5: glob miss + fallbackSrc → fallback branch applies when resolvedSrc is null', () => {
|
|
49
|
+
// Simulate glob miss: empty images map for a /src/assets/ URL
|
|
50
|
+
const images: Record<string, () => Promise<{ default: ImageMetadata }>> = {};
|
|
51
|
+
const src = '/src/assets/images/missing.png';
|
|
52
|
+
const fallbackSrc = '/src/assets/images/default.png';
|
|
53
|
+
|
|
54
|
+
const dispatch = classifySource(src);
|
|
55
|
+
expect(dispatch).toBe('internal-src-assets');
|
|
56
|
+
|
|
57
|
+
// Verify fallback key lookup works
|
|
58
|
+
const hasMainImage = src in images;
|
|
59
|
+
const hasFallback = fallbackSrc in images;
|
|
60
|
+
expect(hasMainImage).toBe(false); // glob miss
|
|
61
|
+
expect(hasFallback).toBe(false); // fallback also missing → plain img fallback
|
|
62
|
+
|
|
63
|
+
// The component would render plain <img> when both are missing (documented in UniversalMedia)
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { notFoundSchema } from '../notFound.js';
|
|
3
|
+
|
|
4
|
+
describe('notFoundSchema', () => {
|
|
5
|
+
it('parses a multi-locale object with partial fields', () => {
|
|
6
|
+
const input = {
|
|
7
|
+
de: { heading: 'Seite nicht gefunden', message: 'Existiert nicht.' },
|
|
8
|
+
en: { heading: 'Not found' },
|
|
9
|
+
};
|
|
10
|
+
expect(() => notFoundSchema.parse(input)).not.toThrow();
|
|
11
|
+
const result = notFoundSchema.parse(input);
|
|
12
|
+
expect(result.de.heading).toBe('Seite nicht gefunden');
|
|
13
|
+
expect(result.en.heading).toBe('Not found');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('parses an empty object (all fields optional)', () => {
|
|
17
|
+
expect(() => notFoundSchema.parse({})).not.toThrow();
|
|
18
|
+
expect(notFoundSchema.parse({})).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses arbitrary locale keys (open record)', () => {
|
|
22
|
+
const input = { de: {}, fr: {}, sq: {} };
|
|
23
|
+
expect(() => notFoundSchema.parse(input)).not.toThrow();
|
|
24
|
+
const result = notFoundSchema.parse(input);
|
|
25
|
+
expect(Object.keys(result)).toEqual(expect.arrayContaining(['de', 'fr', 'sq']));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('rejects a record where image is not a string', () => {
|
|
29
|
+
const input = { de: { image: 42 } };
|
|
30
|
+
expect(() => notFoundSchema.parse(input)).toThrow();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery content collection schema — discriminated union over supported variants.
|
|
3
|
+
*
|
|
4
|
+
* Each entry under a consuming site's `src/content/galleries/` selects a `kind`
|
|
5
|
+
* that determines which Astro component renders it. Pages decide which entry
|
|
6
|
+
* to load and where to place it; the entry decides what to show and how it is
|
|
7
|
+
* laid out.
|
|
8
|
+
*
|
|
9
|
+
* Supported kinds: `image-grid`, `hero-slider`, `carousel`, `masonry-grid`,
|
|
10
|
+
* `feature-highlight`, `lightbox-grid`. Adding a new kind requires updating
|
|
11
|
+
* this union AND adding the matching component in `../components/`. Authoring
|
|
12
|
+
* an unsupported `kind` fails Zod validation at build time.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { defineCollection } from 'astro:content';
|
|
18
|
+
* import { glob } from 'astro/loaders';
|
|
19
|
+
* import { gallerySchema } from '@easy-web/content-blocks/schemas/galleries';
|
|
20
|
+
*
|
|
21
|
+
* const galleries = defineCollection({
|
|
22
|
+
* loader: glob({ pattern: '**\/*.{yml,yaml}', base: './src/content/galleries' }),
|
|
23
|
+
* schema: gallerySchema,
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
|
|
29
|
+
const galleryImageGridSchema = z.object({
|
|
30
|
+
kind: z.literal('image-grid'),
|
|
31
|
+
title: z.string().optional(),
|
|
32
|
+
locale: z.enum(['de', 'en']),
|
|
33
|
+
translationKey: z.string(),
|
|
34
|
+
columns: z.union([z.literal(2), z.literal(3), z.literal(4), z.literal(6)]).default(3),
|
|
35
|
+
gap: z.enum(['sm', 'md', 'lg']).default('md'),
|
|
36
|
+
aspectRatio: z.enum(['square', '4/3', '16/9', 'auto']).default('square'),
|
|
37
|
+
items: z.array(z.object({
|
|
38
|
+
src: z.string().min(1),
|
|
39
|
+
alt: z.string(),
|
|
40
|
+
caption: z.string().optional(),
|
|
41
|
+
href: z.string().optional(),
|
|
42
|
+
})).min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const galleryHeroSliderSchema = z.object({
|
|
46
|
+
kind: z.literal('hero-slider'),
|
|
47
|
+
title: z.string().optional(),
|
|
48
|
+
locale: z.enum(['de', 'en']),
|
|
49
|
+
translationKey: z.string(),
|
|
50
|
+
autoplay: z.boolean().default(true),
|
|
51
|
+
interval: z.number().int().positive().default(5000),
|
|
52
|
+
height: z.enum(['sm', 'md', 'lg', 'full']).default('lg'),
|
|
53
|
+
slides: z.array(z.object({
|
|
54
|
+
src: z.string().min(1),
|
|
55
|
+
alt: z.string(),
|
|
56
|
+
title: z.string(),
|
|
57
|
+
subtitle: z.string().optional(),
|
|
58
|
+
href: z.string().optional(),
|
|
59
|
+
cta: z.string().optional(),
|
|
60
|
+
})).min(1),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const galleryCarouselSchema = z.object({
|
|
64
|
+
kind: z.literal('carousel'),
|
|
65
|
+
title: z.string().optional(),
|
|
66
|
+
locale: z.enum(['de', 'en']),
|
|
67
|
+
translationKey: z.string(),
|
|
68
|
+
autoplay: z.boolean().default(false),
|
|
69
|
+
interval: z.number().int().positive().default(4000),
|
|
70
|
+
showDots: z.boolean().default(true),
|
|
71
|
+
showArrows: z.boolean().default(true),
|
|
72
|
+
slides: z.array(z.object({
|
|
73
|
+
src: z.string().min(1),
|
|
74
|
+
alt: z.string(),
|
|
75
|
+
caption: z.string().optional(),
|
|
76
|
+
})).min(1),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const galleryMasonryGridSchema = z.object({
|
|
80
|
+
kind: z.literal('masonry-grid'),
|
|
81
|
+
title: z.string().optional(),
|
|
82
|
+
locale: z.enum(['de', 'en']),
|
|
83
|
+
translationKey: z.string(),
|
|
84
|
+
columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).default(3),
|
|
85
|
+
gap: z.enum(['sm', 'md', 'lg']).default('md'),
|
|
86
|
+
items: z.array(z.object({
|
|
87
|
+
src: z.string().min(1),
|
|
88
|
+
alt: z.string(),
|
|
89
|
+
caption: z.string().optional(),
|
|
90
|
+
href: z.string().optional(),
|
|
91
|
+
})).min(1),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const galleryFeatureHighlightSchema = z.object({
|
|
95
|
+
kind: z.literal('feature-highlight'),
|
|
96
|
+
title: z.string().optional(),
|
|
97
|
+
locale: z.enum(['de', 'en']),
|
|
98
|
+
translationKey: z.string(),
|
|
99
|
+
gap: z.enum(['sm', 'md', 'lg']).default('lg'),
|
|
100
|
+
items: z.array(z.object({
|
|
101
|
+
src: z.string().min(1),
|
|
102
|
+
alt: z.string(),
|
|
103
|
+
title: z.string(),
|
|
104
|
+
description: z.string(),
|
|
105
|
+
href: z.string().optional(),
|
|
106
|
+
cta: z.string().optional(),
|
|
107
|
+
imagePosition: z.enum(['left', 'right']).optional(),
|
|
108
|
+
})).min(1),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const galleryLightboxGridSchema = z.object({
|
|
112
|
+
kind: z.literal('lightbox-grid'),
|
|
113
|
+
title: z.string().optional(),
|
|
114
|
+
locale: z.enum(['de', 'en']),
|
|
115
|
+
translationKey: z.string(),
|
|
116
|
+
columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).default(3),
|
|
117
|
+
gap: z.enum(['sm', 'md', 'lg']).default('md'),
|
|
118
|
+
aspectRatio: z.enum(['square', '4/3', 'auto']).default('square'),
|
|
119
|
+
items: z.array(z.object({
|
|
120
|
+
src: z.string().min(1),
|
|
121
|
+
alt: z.string(),
|
|
122
|
+
title: z.string().optional(),
|
|
123
|
+
description: z.string().optional(),
|
|
124
|
+
})).min(1),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const gallerySchema = z.discriminatedUnion('kind', [
|
|
128
|
+
galleryImageGridSchema,
|
|
129
|
+
galleryHeroSliderSchema,
|
|
130
|
+
galleryCarouselSchema,
|
|
131
|
+
galleryMasonryGridSchema,
|
|
132
|
+
galleryFeatureHighlightSchema,
|
|
133
|
+
galleryLightboxGridSchema,
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
export type GalleryEntry = z.infer<typeof gallerySchema>;
|
|
137
|
+
export type GalleryImageGridData = z.infer<typeof galleryImageGridSchema>;
|
|
138
|
+
export type GalleryHeroSliderData = z.infer<typeof galleryHeroSliderSchema>;
|
|
139
|
+
export type GalleryCarouselData = z.infer<typeof galleryCarouselSchema>;
|
|
140
|
+
export type GalleryMasonryGridData = z.infer<typeof galleryMasonryGridSchema>;
|
|
141
|
+
export type GalleryFeatureHighlightData = z.infer<typeof galleryFeatureHighlightSchema>;
|
|
142
|
+
export type GalleryLightboxGridData = z.infer<typeof galleryLightboxGridSchema>;
|
|
143
|
+
|
|
144
|
+
export const GALLERY_KINDS = [
|
|
145
|
+
'image-grid',
|
|
146
|
+
'hero-slider',
|
|
147
|
+
'carousel',
|
|
148
|
+
'masonry-grid',
|
|
149
|
+
'feature-highlight',
|
|
150
|
+
'lightbox-grid',
|
|
151
|
+
] as const;
|
|
152
|
+
export type GalleryKind = typeof GALLERY_KINDS[number];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not-found (404) singleton schema — locale-keyed CMS-editable content.
|
|
3
|
+
*
|
|
4
|
+
* Each locale key maps to an optional set of fields rendered by the site's
|
|
5
|
+
* 404 page. All fields are optional so partial translations are valid; a
|
|
6
|
+
* consuming page falls back to defaults when a field is absent.
|
|
7
|
+
*
|
|
8
|
+
* The record key is an open `z.string()` so any IETF locale tag (e.g. `de`,
|
|
9
|
+
* `en`, `fr`, `sq`, `de-AT`) is accepted without schema changes. Do NOT add
|
|
10
|
+
* SEO metadata (title, canonical) here — those belong to the page layer.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { notFoundSchema } from '@easy-web/content-blocks/schemas/notFound';
|
|
16
|
+
*
|
|
17
|
+
* const data = notFoundSchema.parse(await import('../content/not-found.json'));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @see docs/decisions/0011-content-architecture-strategy.md
|
|
21
|
+
* @see docs/decisions/0013-shared-not-found-primitives.md
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
|
|
25
|
+
export const localizedNotFoundSchema = z.object({
|
|
26
|
+
image: z.string().optional(),
|
|
27
|
+
heading: z.string().optional(),
|
|
28
|
+
message: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const notFoundSchema = z.record(z.string(), localizedNotFoundSchema);
|
|
32
|
+
|
|
33
|
+
export type NotFoundData = z.infer<typeof notFoundSchema>;
|