Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@storybook/sveltekit": "^9.1.10",
"@storybook/test-runner": "^0.23.0",
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/enhanced-img": "^0.8.4",
"@sveltejs/kit": "^2.43.7",
"@sveltejs/package": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
Expand Down
4 changes: 4 additions & 0 deletions components/src/DesignTokens/DesignTokens.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
padding: 0;
}

:global(a) {
color: inherit;
}

.container {
display: contents;
--fast: 150ms;
Expand Down
31 changes: 31 additions & 0 deletions components/src/SwrHeader/SwrHeader.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Story, Meta, Primary, Controls, Stories } from '@storybook/addon-docs/blocks';

import * as SwrHeaderStories from './SwrHeader.stories.svelte';

<Meta of={SwrHeaderStories}/>

# SwrHeader

This component replicates the [SWR.de](https://www.swr.de) standard article header. This is useful if we want to replace the original header with our own version, as we did [here](https://www.swr.de/swraktuell/baden-wuerttemberg/wie-staedte-klimaneutral-heizen-wollen-100.html).

<Controls />

Bylines have the following shape:

```ts
interface Byline {
name: string;
image?: string;
}
```

`imageModules` is generated in the application component with:

```ts
const imageModules = import.meta.glob('./test/**.jpg', {
eager: true,
query: { enhanced: true, w: 100 }
});
```

<Stories/>
54 changes: 54 additions & 0 deletions components/src/SwrHeader/SwrHeader.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script context="module">
import { defineMeta } from '@storybook/addon-svelte-csf';
import DesignTokens from '../DesignTokens/DesignTokens.svelte';
import { expect, within } from 'storybook/test';
import SwrHeader from './SwrHeader.svelte';

const { Story } = defineMeta({
title: 'Display/SwrHeader',
component: SwrHeader
});

const imageModules = import.meta.glob('./test/**.jpg', {
eager: true,
query: { enhanced: true, w: 200 }
});
</script>

<Story
name="Default"
asChild
play={async ({ step, canvasElement }) => {
const canvas = within(canvasElement);
await step('Renders names list', async () => {
const containerEl = canvas.getByTestId('byline-names');
expect(containerEl.textContent).toBe('Von Katharina Forstmair, Tom Burggraf, SWR Data Lab');
});
await step('Renders date', async () => {
const containerEl = canvas.getByTestId('updated');
expect(containerEl.textContent).toBe('Stand: 10.1.2025');
});
}}
>
<DesignTokens theme="light">
<SwrHeader
{imageModules}
title="Große Pläne: Wie Städte klimaneutral heizen wollen"
eyebrow="Wärmewende in Baden-Württemberg"
updated="01-10-2025"
bylines={[
{ name: 'Katharina Forstmair', image: './test/forstmair.jpg' },
{ name: 'Tom Burggraf', image: './test/burggraf.jpg' }
]}
>
{#snippet subtitle()}
<span class="intro">
Drei Kommunen, drei Pläne, ein Ziel: Städte und Gemeinden müssen in Wärmeplänen
festhalten, wie in Zukunft vor Ort klimaneutral geheizt werden soll. Die Pläne von
Stuttgart, Lörrach und Vaihingen zeigen, vor welchen Herausforderungen das Land bei der
Wärmewende steht.
</span>
{/snippet}
</SwrHeader>
</DesignTokens>
</Story>
155 changes: 155 additions & 0 deletions components/src/SwrHeader/SwrHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script lang="ts">
import { getContext, type Snippet } from 'svelte';

import Caption from '../Caption/Caption.svelte';

interface Byline {
name: string;
image?: string;
}

interface SwrHeaderProps {
title: string;
subtitle?: Snippet;
eyebrow?: string;
imageModules?: Record<string, any>;
updated?: Date | string;
bylines?: Byline[];
}

let theme = getContext('theme');
const {
title,
subtitle,
eyebrow,
imageModules,
updated,
bylines = []
}: SwrHeaderProps = $props();

const updated_on = updated ? (updated instanceof Date ? updated : new Date(updated)) : null;
</script>

<header class={`container theme-${theme}`}>
{#if eyebrow}
<p class="eyebrow">{eyebrow}</p>
{/if}
<h1 class="title">{title}</h1>
{#if subtitle}
<p class="subtitle">
{@render subtitle()}
</p>
{/if}
<div class="meta">
{#if bylines && bylines.length > 0}
{@const nameString = `Von ${bylines.map((el) => (el.url ? `<a href='${el.url}'>${el.name}</a>` : el.name)).join(', ')}, <a href="https://www.swr.de/home/swr-data-lab-team-100.html">SWR Data Lab</a>`}
<div class="bylines">
{#if imageModules}
<ul class="byline-images">
{#each bylines.filter((el) => el.image && el.image in imageModules) as b, i}
{@const src = imageModules[b.image].default}
<li class="byline-image" style:z-index={bylines.length - i}>
<enhanced:img {src} alt={b.name} />
</li>
{/each}
</ul>
{/if}
<Caption>
<p data-testid="byline-names" class="byline-names">{@html nameString}</p>
</Caption>
</div>
{/if}
{#if updated_on}
<p class="updated" data-testid="updated">Stand: {updated_on.toLocaleDateString('de-DE')}</p>
{/if}
</div>
</header>

<style lang="scss">
// 14px baseline
.container {
color: var(--color-textPrimary);
font-family: var(--swr-sans);
margin: 0 auto;
margin-bottom: 1rem;
max-width: 44rem;
text-shadow: 0 0 4px white;

&.theme-dark {
text-shadow: 0 0 8px rgba(black, 0.5);
}
}
.eyebrow {
font-size: var(--fs-small-1);
margin-bottom: 1.3em;
line-height: 1;
letter-spacing: 0.025em;
}
.title {
font-family: var(--swr-sans);
line-height: 1.175;
letter-spacing: 0.002em;
font-size: var(--fs-large-3);
font-weight: 700;
text-wrap: balance;
}
.subtitle {
margin-top: 1.15em;
font-family: var(--swr-sans);
line-height: 1.4;
font-size: var(--fs-base);
font-weight: 400;
text-shadow: none;
hyphens: auto;
}
.meta {
margin-top: 1.5em;
display: flex;
align-items: center;
column-gap: 2em;
flex-wrap: wrap;
}
.bylines {
display: flex;
flex-flow: column;
gap: 0.5em;
@media (min-width: 500px) {
gap: 1em;
align-items: center;
flex-flow: row;
}
}
.byline-images {
display: flex;
}
.byline-image {
position: relative;
list-style: none;
overflow: hidden;
margin-right: -0.5em;
box-shadow: 0 1px 2px 1px rgba(black, 0.15);
border-radius: 1000px;
border: 1px solid var(--color-pageFill);
img {
width: 2.25rem;
height: auto;
display: block;
}
}
.byline-names {
line-height: 1.35;
}
.updated {
color: var(--color-textSecondary);
font-size: var(--fs-small-2);
letter-spacing: 0.025em;
}
:global(a) {
text-underline-offset: 0.25em;
text-decoration-color: var(--gray-light-1);
&:hover,
&:focus-visible {
text-decoration-color: currentColor;
}
}
</style>
2 changes: 2 additions & 0 deletions components/src/SwrHeader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import SwrHeader from './SwrHeader.svelte';
export default SwrHeader;
Binary file added components/src/SwrHeader/test/burggraf.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added components/src/SwrHeader/test/forstmair.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as Note } from './Note/Note.svelte';
// Display
export { default as Card } from './Card/Card.svelte';
export { default as Scroller } from './Scroller/Scroller.svelte';
export { default as SwrHeader } from './SwrHeader/SwrHeader.svelte';

// Chart
export { default as ChartHeader } from './ChartHeader/ChartHeader.svelte';
Expand Down
3 changes: 2 additions & 1 deletion components/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { enhancedImages } from '@sveltejs/enhanced-img';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [sveltekit()]
plugins: [enhancedImages(), sveltekit()]
});
Loading