{"_id":"@achimismaili/easy-web-content-blocks","_rev":"5-4d20b68a9f332aba17618cad48803b21","name":"@achimismaili/easy-web-content-blocks","dist-tags":{"latest":"1.1.0"},"versions":{"0.6.0":{"name":"@achimismaili/easy-web-content-blocks","version":"0.6.0","license":"UNLICENSED","_id":"@achimismaili/easy-web-content-blocks@0.6.0","maintainers":[{"name":"achimismaili","email":"achim@ismaili.de"}],"homepage":"https://github.com/achimismaili/easy-web#readme","bugs":{"url":"https://github.com/achimismaili/easy-web/issues"},"dist":{"shasum":"bd826ddd80ce98258f40659e09f44483e5a0e578","tarball":"https://registry.npmjs.org/@achimismaili/easy-web-content-blocks/-/easy-web-content-blocks-0.6.0.tgz","fileCount":38,"integrity":"sha512-fBgHzSoc0XxByBEswZI6RZWpA861KFDUxJ/Rqvy956oE7UablCGIiw+i9dc3Ics4562oh4UChLU6MSGcYdF7Gg==","signatures":[{"sig":"MEUCIDchex7ycUw9031/AyZWZWIsucIGAy3T2+M+zSvDODBMAiEAuud/MvM+YuSPU3y4FBZimJu4IDXWMiXAjFA6VIBmbRI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":146382},"type":"module","_from":"file:achimismaili-easy-web-content-blocks-0.6.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./schemas/*":"./src/schemas/*.ts","./components/*":"./src/components/*.astro"},"scripts":{"build":"tsc","typecheck":"tsc --noEmit"},"_npmUser":{"name":"achimismaili","email":"achim@ismaili.de"},"_resolved":"C:\\Users\\ACHIMI~1\\AppData\\Local\\Temp\\9e30078895712c6bb9c83af0472eb7d6\\achimismaili-easy-web-content-blocks-0.6.0.tgz","_integrity":"sha512-fBgHzSoc0XxByBEswZI6RZWpA861KFDUxJ/Rqvy956oE7UablCGIiw+i9dc3Ics4562oh4UChLU6MSGcYdF7Gg==","repository":{"url":"git+https://github.com/achimismaili/easy-web.git","type":"git","directory":"packages/easy-web-content-blocks"},"_npmVersion":"10.9.8","description":"Reusable Astro content-block components for the easy-web ecosystem.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.0.0","astro":"^6.4.0","typescript":"^5.5.0"},"peerDependencies":{"zod":">=4.0.0","astro":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/easy-web-content-blocks_0.6.0_1782497129355_0.7574365543753347","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @easy-web/content-blocks. Install that instead; this package is no longer updated."},"0.6.1":{"name":"@achimismaili/easy-web-content-blocks","version":"0.6.1","license":"UNLICENSED","_id":"@achimismaili/easy-web-content-blocks@0.6.1","maintainers":[{"name":"achimismaili","email":"achim@ismaili.de"}],"homepage":"https://github.com/achimismaili/easy-web#readme","bugs":{"url":"https://github.com/achimismaili/easy-web/issues"},"dist":{"shasum":"6acb2b709ade4942bf3f1c17fbf0955e442d077a","tarball":"https://registry.npmjs.org/@achimismaili/easy-web-content-blocks/-/easy-web-content-blocks-0.6.1.tgz","fileCount":38,"integrity":"sha512-aaK3iGI7jpgZHB0TSIRn3vc/fiPBboVhHMZs+x9dBZ75Nvc6vfX0zGLg5SX8wa426ck6Gm9dafne4uSxkJhrjQ==","signatures":[{"sig":"MEQCIHEvDSonxU0g8GxjOW1vRmkKG3LBm7q9Hce6ZK/IJrVmAiA29j2XbiXGDzNHF56eFUo4djR2uFQ6nkLdCaBVhF4k+A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@achimismaili%2feasy-web-content-blocks@0.6.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":146946},"type":"module","_from":"file:achimismaili-easy-web-content-blocks-0.6.1.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./schemas/*":"./src/schemas/*.ts","./components/*":"./src/components/*.astro"},"scripts":{"build":"tsc","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c565fbbe-0225-450d-8182-6ac2aa0c7c6e"}},"_resolved":"/tmp/0fc3f7aea7f18351157ea81a4238199b/achimismaili-easy-web-content-blocks-0.6.1.tgz","_integrity":"sha512-aaK3iGI7jpgZHB0TSIRn3vc/fiPBboVhHMZs+x9dBZ75Nvc6vfX0zGLg5SX8wa426ck6Gm9dafne4uSxkJhrjQ==","repository":{"url":"git+https://github.com/achimismaili/easy-web.git","type":"git","directory":"packages/easy-web-content-blocks"},"_npmVersion":"11.18.0","description":"Reusable Astro content-block components for the easy-web ecosystem.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.0.0","astro":"^6.4.0","typescript":"^5.5.0","@achimismaili/easy-web-theme-core":"0.5.0"},"peerDependencies":{"zod":">=4.0.0","astro":">=6.0.0","@achimismaili/easy-web-theme-core":"0.5.0"},"_npmOperationalInternal":{"tmp":"tmp/easy-web-content-blocks_0.6.1_1783027853188_0.048086970522494","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @easy-web/content-blocks. Install that instead; this package is no longer updated."},"1.0.0":{"name":"@achimismaili/easy-web-content-blocks","version":"1.0.0","license":"MIT","_id":"@achimismaili/easy-web-content-blocks@1.0.0","maintainers":[{"name":"achimismaili","email":"achim@ismaili.de"}],"homepage":"https://github.com/achimismaili/easy-web#readme","bugs":{"url":"https://github.com/achimismaili/easy-web/issues"},"dist":{"shasum":"dc88e1f0e5f266677d99a99f85a0b873d0001b19","tarball":"https://registry.npmjs.org/@achimismaili/easy-web-content-blocks/-/easy-web-content-blocks-1.0.0.tgz","fileCount":44,"integrity":"sha512-yNsSL3UgDmFy0YDd+T6/qlqfBjpKb5m5nOwwG5+PJXN5NdThJf5UYBXagkAOwSyn1oK5yRukWzNHQXwCziafQw==","signatures":[{"sig":"MEUCIFpRlnw93byVHXwDJHjROuYu4ygqbC2wlXjbajd5Sr51AiEAmvScrjTo1/7wYIItMJZqZIJFux3b6wYpVg3uFjtBs4Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@achimismaili%2feasy-web-content-blocks@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":159130},"type":"module","_from":"file:achimismaili-easy-web-content-blocks-1.0.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./schemas/*":"./src/schemas/*.ts","./components/*":"./src/components/*.astro"},"scripts":{"test":"vitest run","build":"tsc","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c565fbbe-0225-450d-8182-6ac2aa0c7c6e"}},"_resolved":"/tmp/63c51b2d7e82a09fda98e23f362130b8/achimismaili-easy-web-content-blocks-1.0.0.tgz","_integrity":"sha512-yNsSL3UgDmFy0YDd+T6/qlqfBjpKb5m5nOwwG5+PJXN5NdThJf5UYBXagkAOwSyn1oK5yRukWzNHQXwCziafQw==","repository":{"url":"git+https://github.com/achimismaili/easy-web.git","type":"git","directory":"packages/easy-web-content-blocks"},"_npmVersion":"12.0.1","description":"Reusable Astro content-block components for the easy-web ecosystem.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.0.0","astro":"^6.4.0","vitest":"^4.1.10","typescript":"^5.5.0","@achimismaili/easy-web-theme-core":"1.0.0"},"peerDependencies":{"zod":">=4.0.0","astro":">=6.0.0 <8.0.0","@achimismaili/easy-web-theme-core":"1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/easy-web-content-blocks_1.0.0_1784214873904_0.5296873395159678","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @easy-web/content-blocks. Install that instead; this package is no longer updated."},"1.1.0":{"name":"@achimismaili/easy-web-content-blocks","version":"1.1.0","license":"MIT","_id":"@achimismaili/easy-web-content-blocks@1.1.0","maintainers":[{"name":"achimismaili","email":"achim@ismaili.de"}],"homepage":"https://github.com/achimismaili/easy-web#readme","bugs":{"url":"https://github.com/achimismaili/easy-web/issues"},"dist":{"shasum":"0c0ffd67851b00f5a7fd199bcd5ccfc830285a3b","tarball":"https://registry.npmjs.org/@achimismaili/easy-web-content-blocks/-/easy-web-content-blocks-1.1.0.tgz","fileCount":55,"integrity":"sha512-sSy0qzKTanTB8QK3JRVKgfJCGn8qPbuqMfXTMMnRodvSEYtXyNV//ZcRfQKMOh43SRpDksk9b1IZYdEx0To4wA==","signatures":[{"sig":"MEYCIQCV/zMNW9hnm376ATDLkT5Yd5UFSaEIbS+XSa9KyQEL7QIhAJ3l9V/2b/CHGbOjL9QEhIcgk/GRbJ4EwVsvrSUEnkCF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@achimismaili%2feasy-web-content-blocks@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":172177},"type":"module","_from":"file:achimismaili-easy-web-content-blocks-1.1.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./schemas/*":"./src/schemas/*.ts","./components/*":"./src/components/*.astro"},"scripts":{"test":"vitest run","build":"tsc","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c565fbbe-0225-450d-8182-6ac2aa0c7c6e"}},"_resolved":"/tmp/0a5dcb3f48a53604456f7f4eeb422e18/achimismaili-easy-web-content-blocks-1.1.0.tgz","_integrity":"sha512-sSy0qzKTanTB8QK3JRVKgfJCGn8qPbuqMfXTMMnRodvSEYtXyNV//ZcRfQKMOh43SRpDksk9b1IZYdEx0To4wA==","repository":{"url":"git+https://github.com/achimismaili/easy-web.git","type":"git","directory":"packages/easy-web-content-blocks"},"_npmVersion":"12.0.1","description":"Reusable Astro content-block components for the easy-web ecosystem.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.0.0","astro":"^6.4.0","vitest":"^4.1.10","typescript":"^5.5.0","@achimismaili/easy-web-theme-core":"1.0.0"},"peerDependencies":{"zod":">=4.0.0","astro":">=6.0.0 <8.0.0","@achimismaili/easy-web-i18n":">=1.0.0","@achimismaili/easy-web-theme-core":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/easy-web-content-blocks_1.1.0_1784220538603_0.3260824522933934","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Moved to @easy-web/content-blocks. Install that instead; this package is no longer updated."}},"time":{"created":"2026-06-26T18:05:29.229Z","modified":"2026-08-30T11:43:34.690Z","0.6.0":"2026-06-26T18:05:29.519Z","0.6.1":"2026-07-02T21:30:53.312Z","1.0.0":"2026-07-16T15:14:34.091Z","1.1.0":"2026-07-16T16:48:58.739Z"},"bugs":{"url":"https://github.com/achimismaili/easy-web/issues"},"license":"MIT","homepage":"https://github.com/achimismaili/easy-web#readme","repository":{"url":"git+https://github.com/achimismaili/easy-web.git","type":"git","directory":"packages/easy-web-content-blocks"},"description":"Reusable Astro content-block components for the easy-web ecosystem.","maintainers":[{"name":"achimismaili","email":"achim@ismaili.de"}],"readme":"# @achimismaili/easy-web-content-blocks\n\nReusable 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 `@achimismaili/easy-web-theme-core`; no styling system of their own.\n\n## Installation\n\n```bash\npnpm add @achimismaili/easy-web-content-blocks\n```\n\nThis package is published to npm. Install it like any other npm package.\n\n### Peer dependencies\n\n* `astro >= 6.0.0`\n\nYou almost certainly also want `@achimismaili/easy-web-theme-core` (for the `--ew-*` token CSS) and, if you build a bilingual site, `@achimismaili/easy-web-i18n` (for the alternate-link helper used by `LanguageSwitch`).\n\n## Import convention\n\nComponents ship as **raw `.astro` files**. There is no JavaScript barrel. Import each component from its component path:\n\n```astro\n---\nimport Header from '@achimismaili/easy-web-content-blocks/components/Header';\nimport Footer from '@achimismaili/easy-web-content-blocks/components/Footer';\nimport Hero from '@achimismaili/easy-web-content-blocks/components/Hero';\nimport Section from '@achimismaili/easy-web-content-blocks/components/Section';\nimport CardGrid from '@achimismaili/easy-web-content-blocks/components/CardGrid';\nimport Card from '@achimismaili/easy-web-content-blocks/components/Card';\n---\n```\n\nThe 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.\n\n## Quick start\n\n### Layout — wire Header and Footer into your base layout\n\n```astro\n---\n// src/layouts/Base.astro\nimport '@achimismaili/easy-web-theme-core/tokens.css';\nimport { noFlashScript } from '@achimismaili/easy-web-theme-core';\nimport Header from '@achimismaili/easy-web-content-blocks/components/Header';\nimport Footer from '@achimismaili/easy-web-content-blocks/components/Footer';\nimport { siteConfig } from '../config';\n\ninterface Props {\n  lang: string;\n  title: string;\n  pathname: string;\n  alternateHref?: string;\n}\nconst { lang, title, pathname, alternateHref } = Astro.props;\nconst navItems = siteConfig.navItems[lang];\nconst legalLinks = siteConfig.legalLinks[lang];\n---\n<html lang={lang}>\n  <head>\n    <script is:inline set:html={noFlashScript}></script>\n    <title>{title}</title>\n  </head>\n  <body>\n    <Header\n      siteName={siteConfig.siteName}\n      navItems={navItems}\n      currentLang={lang}\n      pathname={pathname}\n      alternateHref={alternateHref}\n    />\n    <slot />\n    <Footer siteName={siteConfig.siteName} legalLinks={legalLinks} />\n  </body>\n</html>\n```\n\n### Page — compose Hero + Section + CardGrid + Card\n\n```astro\n---\n// src/pages/index.astro\nimport Layout from '../layouts/Base.astro';\nimport Hero from '@achimismaili/easy-web-content-blocks/components/Hero';\nimport Section from '@achimismaili/easy-web-content-blocks/components/Section';\nimport CardGrid from '@achimismaili/easy-web-content-blocks/components/CardGrid';\nimport Card from '@achimismaili/easy-web-content-blocks/components/Card';\n---\n<Layout lang=\"de\" title=\"Home\" pathname=\"/\">\n  <Hero\n    title=\"Willkommen\"\n    subtitle=\"Untertitel mit Beschreibung\"\n    ctaLabel=\"Mehr erfahren\"\n    ctaHref=\"#about\"\n  />\n  <Section id=\"about\">\n    <h2>Über uns</h2>\n    <p>Inhalt …</p>\n  </Section>\n  <Section>\n    <CardGrid>\n      <Card title=\"Erste Karte\" description=\"Kurze Beschreibung\" href=\"/a\" />\n      <Card title=\"Zweite Karte\" description=\"Kurze Beschreibung\" href=\"/b\" />\n      <Card title=\"Dritte Karte\" description=\"Kurze Beschreibung\" href=\"/c\" />\n    </CardGrid>\n  </Section>\n</Layout>\n```\n\n## Component reference\n\nAll components use the `--ew-*` design tokens from `@achimismaili/easy-web-theme-core`. None ship their own theme system; they inherit whatever tokens are loaded in the consuming page.\n\n### Site chrome\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `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\"`. |\n| `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\"`. |\n| `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\"`. |\n| `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\"`. |\n| `Footer` | `siteName: string`, `legalLinks: Array<{ label: string; href: string }>` | — | Simple copyright + legal-link footer. |\n| `ThemeToggle` | — | — | Light / dark / system trio button. Talks to `@achimismaili/easy-web-theme-core` via `data-theme` on `<html>` and `localStorage`. |\n| `LanguageSwitch` | `currentLang: string`, `pathname: string` | `alternateHref?: string` | DE ↔ EN switcher. If `alternateHref` is provided, links there directly; otherwise infers the alternate path from `pathname`. |\n\n#### Header `actions` slot\n\nAll 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).\n\n```astro\n---\nimport Header from '@achimismaili/easy-web-content-blocks/components/Header';\nimport HeaderCentered from '@achimismaili/easy-web-content-blocks/components/HeaderCentered';\nimport HeaderHideOnScroll from '@achimismaili/easy-web-content-blocks/components/HeaderHideOnScroll';\nimport HeaderFlyout from '@achimismaili/easy-web-content-blocks/components/HeaderFlyout';\n---\n\n<!-- Default: ThemeToggle + LanguageSwitch rendered automatically -->\n<Header siteName=\"My Site\" navItems={navItems} currentLang=\"de\" pathname={pathname} />\n\n<!-- Custom slot content replaces ThemeToggle + LanguageSwitch -->\n<HeaderCentered siteName=\"My Site\" navItems={navItems} currentLang=\"de\" pathname={pathname}>\n  <fragment slot=\"actions\">\n    <MyCustomButton />\n  </fragment>\n</HeaderCentered>\n\n<!-- Flyout: navItems may include children[] for dropdown panels -->\n<HeaderFlyout siteName=\"My Site\" navItems={navItemsWithChildren} currentLang=\"de\" pathname={pathname} />\n```\n\nSelecting the header variant in a Base layout via a `headerVariant` prop:\n\n```astro\n---\n// Base.astro\nimport Header from '@achimismaili/easy-web-content-blocks/components/Header';\nimport HeaderCentered from '@achimismaili/easy-web-content-blocks/components/HeaderCentered';\nimport HeaderHideOnScroll from '@achimismaili/easy-web-content-blocks/components/HeaderHideOnScroll';\nimport HeaderFlyout from '@achimismaili/easy-web-content-blocks/components/HeaderFlyout';\n\ninterface Props {\n  headerVariant?: 'classic' | 'centered' | 'hide-on-scroll' | 'flyout';\n  // …other props\n}\nconst { headerVariant = 'classic' } = Astro.props;\nconst HeaderComponents = { classic: Header, centered: HeaderCentered, 'hide-on-scroll': HeaderHideOnScroll, flyout: HeaderFlyout };\nconst ActiveHeader = HeaderComponents[headerVariant];\n---\n<ActiveHeader siteName={siteName} navItems={navItems} currentLang={lang} pathname={pathname} alternateHref={alternateHref} />\n```\n\n### Hero / call-to-action / contact\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `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. |\n| `CtaSection` | `heading: string`, `buttonLabel: string`, `buttonHref: string` | `body?: string`, `variant?: 'default' \\| 'muted' \\| 'primary'` | Standalone CTA banner. `primary` variant inverts colors to brand. |\n| `ContactSection` | `heading: string`, `email: string`, `buttonLabel: string` | `body?: string` | Centered mailto-only contact CTA. Button is `mailto:${email}`. |\n\n### Structural\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `Section` | — | `id?: string`, `class?: string` | Generic content wrapper. `id` enables anchor-link targets. Slot-based. |\n| `CardGrid` | — | — | Responsive CSS grid (3 → 2 → 1 columns by breakpoint). Slot in `Card`s. |\n| `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\"`. |\n\n### Gallery system\n\nThe 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.\n\nSix variants are available:\n\n| `kind` | Component | Use case |\n| :--- | :--- | :--- |\n| `image-grid` | `GalleryImageGrid` | Static N-column uniform grid. Component previews, team photos, sponsors. |\n| `hero-slider` | `GalleryHeroSlider` | Full-bleed editorial carousel with title + subtitle + optional CTA per slide. |\n| `carousel` | `GalleryCarousel` | Image slider with optional captions. Constrained-width container; no per-slide CTA. |\n| `masonry-grid` | `GalleryMasonryGrid` | Pinterest-style variable-height grid via CSS columns. Photography, portfolios. |\n| `feature-highlight` | `GalleryFeatureHighlight` | Alternating image+text+link rows. Product feature showcase, service descriptions. |\n| `lightbox-grid` | `GalleryLightboxGrid` | Uniform grid + click-to-expand modal. Photo albums, event galleries. Vanilla JS modal with keyboard navigation. |\n\nSchema is exported from this package and consumed by each instance's `content.config.ts`:\n\n```ts\nimport { defineCollection } from 'astro:content';\nimport { glob } from 'astro/loaders';\nimport { gallerySchema } from '@achimismaili/easy-web-content-blocks/schemas/galleries';\n\nconst galleries = defineCollection({\n  loader: glob({ pattern: '**/*.{yml,yaml}', base: './src/content/galleries' }),\n  schema: gallerySchema,\n});\n```\n\nUse the `GallerySection` dispatcher in pages — it switches on `entry.data.kind` and renders the right variant:\n\n```astro\n---\nimport GallerySection from '@achimismaili/easy-web-content-blocks/components/GallerySection';\nimport { getEntry } from 'astro:content';\n\nconst heroEntry = await getEntry('galleries', 'home-hero-de');\nconst previewEntry = await getEntry('galleries', 'about-component-preview-de');\n---\n<GallerySection entry={heroEntry} />\n<!-- ...other page content... -->\n<GallerySection entry={previewEntry} />\n```\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `GallerySection` | `entry: { data: GalleryEntry }` | — | Dispatcher. Renders the correct variant based on `entry.data.kind`. Renders nothing when `entry` is null/undefined. |\n| `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>`. |\n| `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`. |\n| `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`. |\n| `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. |\n| `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). |\n| `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. |\n\n### Blog\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `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`. |\n\n### Legal & long-form\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `LegalLayout` | — | (slot-based) | Narrow content wrapper for impressum / datenschutz / AGB. Constrains line-length for legal prose. |\n| `Prose` | — | (slot-based) | Styled wrapper for Markdown/HTML content (`<h1>`–`<h6>`, `<p>`, `<ul>`, `<a>`, `<blockquote>`, `<code>`, etc.) using `--ew-*` typography tokens. |\n\n### Banners / notices\n\n| Component | Required props | Optional props | Notes |\n| :--- | :--- | :--- | :--- |\n| `DraftBanner` | — | (slot-based, expects a short message) | Banner notice for draft content. |\n| `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. |\n\n## <UniversalMedia>\n\nA universal image dispatcher component that renders images from any source type consistently.\n\n### Props\n\n| Prop | Type | Required | Description |\n|------|------|----------|-------------|\n| `src` | `ImageMetadata \\| string` | ✓ | Image source: resolved metadata or a URL/path string |\n| `alt` | `string` | ✓ | Alt text |\n| `images` | `Record<string, () => Promise<{ default: ImageMetadata }>>` | ✓ | Consumer-supplied glob result (pass `import.meta.glob(...)` from the calling component) |\n| `fallbackSrc` | `string` | — | Fallback image path when `/src/assets/` glob lookup fails |\n| `fallbackAlt` | `string` | — | Fallback alt text (defaults to `alt`) |\n| `width` | `number` | — | Image width in px (default: 800) |\n| `height` | `number` | — | Image height in px (default: 600) |\n| `class` | `string` | — | CSS class name |\n| `decoding` | `'async' \\| 'auto' \\| 'sync'` | — | Image decoding hint (default: `'async'`) |\n\n### Dispatch logic\n\n1. **`ImageMetadata` object** → `<Image>` (Astro-optimised, generates srcset)\n2. **`/src/assets/...` string** → glob lookup via `images` prop → `<Image>` (or fallback/plain `<img>` on miss)\n3. **`http://...` / `https://...` string** → `<Image inferSize>` (reads dimensions from remote)\n4. **Everything else** (e.g., `/public/...` paths) → plain `<img>`\n\n> **Unsupported formats**: HEIC and JXL. Reject these at the CMS upload level (e.g., Decap CMS widget `hint`).\n\n### Usage\n\n```astro\n---\nimport type { ImageMetadata } from 'astro';\nimport UniversalMedia from '@achimismaili/easy-web-content-blocks/components/UniversalMedia';\n\nconst images = import.meta.glob<{ default: ImageMetadata }>('/src/assets/**', { import: 'default' });\n---\n\n<UniversalMedia\n  src={entry.data.photo}\n  alt={entry.data.photoAlt}\n  {images}\n  fallbackSrc=\"/src/assets/images/default.png\"\n  fallbackAlt=\"Default image\"\n/>\n```\n\n## Styling\n\nAll components reference the `@achimismaili/easy-web-theme-core` CSS custom properties:\n\n* Colors: `--ew-surface`, `--ew-on-surface`, `--ew-surface-muted`, `--ew-primary`, `--ew-on-primary`, `--ew-border`, `--ew-muted`\n* Typography: `--ew-font-sans`, `--ew-text-{xs,sm,base,lg,xl,2xl,3xl}`, `--ew-leading-{tight,normal,loose}`\n* Spacing: `--ew-space-{0..12}`\n* Radii: `--ew-radius-{sm,md,lg,xl,full}`\n\nTo re-skin a site, override these custom properties on a parent element (or globally on `:root`) — do not edit the components.\n\nComponents 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`.\n\n## Adoption status\n\nThe [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:\n\n* `dev.ismaili.de` (pilot) — `^0.2.0`\n* `harleyrentflorida.de` (customer) — `^0.1.0` (one minor behind; bump pending while `/preview` is paused)\n\n## See also\n\n* [`@achimismaili/easy-web-theme-core`](https://github.com/achimismaili/easy-web/blob/main/packages/theme-core/README.md) — the design-token package these components consume.\n* [`@achimismaili/easy-web-i18n`](https://github.com/achimismaili/easy-web/blob/main/packages/i18n/README.md) — Astro + Paraglide bilingual utilities used together with `LanguageSwitch`.\n* [`easy-web/AGENTS.md`](https://github.com/achimismaili/easy-web/blob/main/AGENTS.md) — repo orientation and publishing workflow.\n","readmeFilename":"README.md"}