feat(shared): add MetaLine primitive — · -separated meta, optional icon (§7)

Refs #859
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-16 19:15:48 +02:00
parent fa510f3991
commit 649b6b447c
2 changed files with 100 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<script lang="ts">
let {
items,
iconSrc
}: {
items: string[];
iconSrc?: string;
} = $props();
</script>
{#if items.length > 0}
<div
data-testid="meta-line"
style="display:flex; align-items:center; flex-wrap:wrap; gap:8px; font-family:var(--font-sans); font-size:12px; color:var(--c-ink-2);"
>
{#if iconSrc}
<img src={iconSrc} alt="" style="width:14px; height:14px; opacity:0.5; flex-shrink:0;" />
{/if}
{#each items as item, i (i)}
{#if i > 0}
<span data-testid="meta-sep" aria-hidden="true">·</span>
{/if}
<span data-testid="meta-item">{item}</span>
{/each}
</div>
{/if}

View File

@@ -0,0 +1,74 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import MetaLine from './MetaLine.svelte';
afterEach(() => cleanup());
describe('MetaLine', () => {
it('renders N item spans when given N items', async () => {
render(MetaLine, { items: ['14. März 1923', '14 Dokumente', '4 Personen'] });
const spans = document.querySelectorAll('[data-testid="meta-item"]');
expect(spans).toHaveLength(3);
expect(spans[0].textContent).toBe('14. März 1923');
expect(spans[1].textContent).toBe('14 Dokumente');
expect(spans[2].textContent).toBe('4 Personen');
});
it('renders separator spans between items', async () => {
render(MetaLine, { items: ['A', 'B', 'C'] });
const seps = document.querySelectorAll('[data-testid="meta-sep"]');
// N items → N-1 separators
expect(seps).toHaveLength(2);
expect(seps[0].textContent).toBe('·');
});
it('renders nothing when items is empty', async () => {
const { container } = render(MetaLine, { items: [] });
// No element children — Svelte may leave an empty comment node but no DOM elements
expect(container.querySelectorAll('[data-testid]')).toHaveLength(0);
expect(container.querySelectorAll('div, span, img')).toHaveLength(0);
});
it('renders nothing when items has one element (no separator)', async () => {
render(MetaLine, { items: ['Nur eines'] });
const seps = document.querySelectorAll('[data-testid="meta-sep"]');
expect(seps).toHaveLength(0);
const spans = document.querySelectorAll('[data-testid="meta-item"]');
expect(spans).toHaveLength(1);
});
it('shows the leading img when iconSrc is supplied', async () => {
render(MetaLine, {
items: ['Datum'],
iconSrc: '/degruyter-icons/Simple/Small-16px/SVG/Action/Calendar-Add-SM.svg'
});
const img = document.querySelector('img');
expect(img).not.toBeNull();
});
it('does NOT render an img when iconSrc is omitted', async () => {
render(MetaLine, { items: ['Datum'] });
const img = document.querySelector('img');
expect(img).toBeNull();
});
it('icon has width 14px, height 14px, opacity 0.5, and alt=""', async () => {
render(MetaLine, {
items: ['Datum'],
iconSrc: '/degruyter-icons/Simple/Small-16px/SVG/Action/Calendar-Add-SM.svg'
});
const img = document.querySelector('img') as HTMLImageElement;
expect(img.alt).toBe('');
// Inline style values (set directly on the element, not via getComputedStyle)
expect(img.style.width).toBe('14px');
expect(img.style.height).toBe('14px');
expect(img.style.opacity).toBe('0.5');
});
it('applies font-size 12px to the wrapper', async () => {
render(MetaLine, { items: ['Test'] });
const wrapper = document.querySelector('[data-testid="meta-line"]') as HTMLElement;
expect(wrapper).not.toBeNull();
expect(wrapper.style.fontSize).toBe('12px');
});
});