diff --git a/frontend/.prettierignore b/frontend/.prettierignore
index 458412f8..d6b62635 100644
--- a/frontend/.prettierignore
+++ b/frontend/.prettierignore
@@ -11,6 +11,7 @@ bun.lockb
# Build artifacts
/.svelte-kit/
/.svelte-kit-backup/
+/.svelte-kit.old/
# Generated files
/.svelte-kit-backup/
diff --git a/frontend/e2e/person-mention-read.spec.ts b/frontend/e2e/person-mention-read.spec.ts
new file mode 100644
index 00000000..17b23c7a
--- /dev/null
+++ b/frontend/e2e/person-mention-read.spec.ts
@@ -0,0 +1,163 @@
+import { test, expect, devices } from '@playwright/test';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import fs from 'fs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
+const STORAGE_STATE = path.resolve(__dirname, '.auth/user.json');
+
+/**
+ * E2E for issue #362 — Person @mentions, read-mode rendering + hover card (B20/B21).
+ *
+ * Strategy:
+ * - Create a document, a Person, and a transcription block whose text contains
+ * `@DisplayName` and whose mentionedPersons sidecar links to that person.
+ * - Open the document in read mode.
+ * - B20: page.hover() on the .person-mention link → hover card mounts.
+ * - B21: with context.setHasTouch(true), page.tap() on the link → navigates
+ * to /persons/{id} without ever showing the hover card.
+ */
+
+let docId: string;
+let personId: string;
+let docHref: string;
+
+test.describe.configure({ mode: 'serial' });
+
+test.describe('Person mention — read mode', () => {
+ test.beforeAll(async ({ request }) => {
+ const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
+
+ // 1. Person we will mention.
+ const personRes = await request.post('/api/persons', {
+ data: {
+ firstName: 'Auguste',
+ lastName: 'Raddatz',
+ personType: 'PERSON',
+ birthYear: 1882,
+ deathYear: 1944
+ }
+ });
+ if (!personRes.ok()) throw new Error(`Create person failed: ${personRes.status()}`);
+ const person = await personRes.json();
+ personId = person.id;
+
+ // 2. Document with a PDF so the transcription panel is mountable.
+ // Sara #3: timestamp the title so a previous run that crashed in beforeAll
+ // (and therefore skipped afterAll cleanup) cannot collide with this one.
+ const uniqueSuffix = Date.now();
+ const docRes = await request.post('/api/documents', {
+ multipart: {
+ title: `E2E Person Mention Read ${uniqueSuffix}`,
+ documentDate: '1945-05-08'
+ }
+ });
+ if (!docRes.ok()) throw new Error(`Create document failed: ${docRes.status()}`);
+ const doc = await docRes.json();
+ docId = doc.id;
+ docHref = `${baseURL}/documents/${docId}`;
+
+ await request.put(`/api/documents/${docId}`, {
+ multipart: {
+ title: doc.title as string,
+ documentDate: '1945-05-08',
+ file: {
+ name: 'minimal.pdf',
+ mimeType: 'application/pdf',
+ buffer: fs.readFileSync(PDF_FIXTURE)
+ }
+ }
+ });
+
+ // 3. Annotation to anchor the block on the page.
+ const annRes = await request.post(`/api/documents/${docId}/annotations`, {
+ data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.5, height: 0.1, color: '#00C7B1' }
+ });
+ if (!annRes.ok()) throw new Error(`Create annotation failed: ${annRes.status()}`);
+
+ // 4. Block text contains @Auguste Raddatz; sidecar links it to personId.
+ const blockRes = await request.post(`/api/documents/${docId}/transcription-blocks`, {
+ data: {
+ pageNumber: 1,
+ x: 0.1,
+ y: 0.1,
+ width: 0.5,
+ height: 0.1,
+ text: 'Brief an @Auguste Raddatz vom Mai 1944',
+ label: null,
+ mentionedPersons: [{ personId, displayName: 'Auguste Raddatz' }]
+ }
+ });
+ if (!blockRes.ok()) throw new Error(`Create block failed: ${blockRes.status()}`);
+ });
+
+ test.afterAll(async ({ request }) => {
+ if (docId) await request.delete(`/api/documents/${docId}`);
+ if (personId) await request.delete(`/api/persons/${personId}`);
+ });
+
+ test('renders the @mention as an underlined anchor link to /persons/{id}', async ({ page }) => {
+ await page.goto(docHref);
+ await page.getByRole('button', { name: 'Transkription' }).click();
+
+ const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
+ await expect(link).toBeVisible({ timeout: 5000 });
+ await expect(link).toHaveAttribute('href', `/persons/${personId}`);
+ // The @ trigger is stripped from the rendered text per spec
+ await expect(link).toHaveText('Auguste Raddatz');
+ });
+
+ test('B20: desktop hover mounts the hover card with loaded person data', async ({ page }) => {
+ await page.goto(docHref);
+ await page.getByRole('button', { name: 'Transkription' }).click();
+
+ const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
+ await link.hover();
+
+ const card = page.getByTestId('person-hover-card');
+ await expect(card).toBeVisible({ timeout: 5000 });
+ // Loaded state: person displayName is rendered inside the card
+ await expect(page.getByTestId('person-hover-card-name')).toHaveText('Auguste Raddatz');
+ // Footer link points to /persons/{id}
+ await expect(card.locator(`a[href="/persons/${personId}"]`)).toBeVisible();
+ });
+
+ test('B20: hover card unmounts on mouseleave', async ({ page }) => {
+ await page.goto(docHref);
+ await page.getByRole('button', { name: 'Transkription' }).click();
+
+ const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
+ await link.hover();
+ await expect(page.getByTestId('person-hover-card')).toBeVisible();
+
+ // Move pointer away
+ await page.mouse.move(0, 0);
+ await expect(page.getByTestId('person-hover-card')).toBeHidden({ timeout: 2000 });
+ });
+
+ test('B21: touch-device tap navigates without showing the hover card', async ({ browser }) => {
+ const context = await browser.newContext({
+ ...devices['Pixel 7'],
+ storageState: STORAGE_STATE
+ });
+ const touchPage = await context.newPage();
+ try {
+ await touchPage.goto(docHref);
+ await touchPage.getByRole('button', { name: 'Transkription' }).click();
+
+ const link = touchPage.locator(`a.person-mention[data-person-id="${personId}"]`).first();
+ await expect(link).toBeVisible({ timeout: 5000 });
+
+ // Sara #2: assert no card *before* the tap so the test actually proves
+ // the touch device suppression worked, not just that we navigated away.
+ await expect(touchPage.getByTestId('person-hover-card')).toHaveCount(0);
+
+ await link.tap();
+ // The card never mounted — the tap navigated directly per spec.
+ await expect(touchPage).toHaveURL(new RegExp(`/persons/${personId}`));
+ } finally {
+ await context.close();
+ }
+ });
+});
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index 3de63ff2..e2312f71 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -12,7 +12,7 @@ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
- { ignores: ['src/paraglide/**'] },
+ { ignores: ['src/paraglide/**', '.svelte-kit.old/**'] },
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index f1b189ce..8ef11f81 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -423,6 +423,7 @@
"person_mention_open_link": "Zur Person",
"person_mention_hover_hint": "Klick öffnet Seite",
"person_mention_load_error": "Person konnte nicht geladen werden.",
+ "person_mention_loading": "Lade Person…",
"person_mention_popup_empty": "Keine Personen gefunden",
"person_mention_btn_label": "Person verlinken",
"person_mention_create_new": "Neue Person anlegen",
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 02111802..c0909263 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -423,6 +423,7 @@
"person_mention_open_link": "Open person",
"person_mention_hover_hint": "Click opens the page",
"person_mention_load_error": "Could not load person.",
+ "person_mention_loading": "Loading person…",
"person_mention_popup_empty": "No persons found",
"person_mention_btn_label": "Link person",
"person_mention_create_new": "Create new person",
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index 5d416017..4b2fcdaf 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -423,6 +423,7 @@
"person_mention_open_link": "Ir a la persona",
"person_mention_hover_hint": "Clic abre la página",
"person_mention_load_error": "No se pudo cargar la persona.",
+ "person_mention_loading": "Cargando persona…",
"person_mention_popup_empty": "No se encontraron personas",
"person_mention_btn_label": "Vincular persona",
"person_mention_create_new": "Crear nueva persona",
diff --git a/frontend/src/lib/components/PersonHoverCard.svelte b/frontend/src/lib/components/PersonHoverCard.svelte
new file mode 100644
index 00000000..36a4989d
--- /dev/null
+++ b/frontend/src/lib/components/PersonHoverCard.svelte
@@ -0,0 +1,270 @@
+
+
+
+ {#if state.status === 'loading'}
+
+ {:else if state.status === 'error'}
+
+ {m.person_mention_load_error()}
+
+
+ {:else}
+
+
+ {#if familyChips.length > 0}
+
+ {#each familyChips as chip (chip.id)}
+ {chip.relatedPersonDisplayName}
+ {/each}
+
+ {/if}
+ {#if notesExcerpt}
+
{notesExcerpt}
+ {/if}
+
+
+ {/if}
+
+
+
diff --git a/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts b/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts
new file mode 100644
index 00000000..f68bfdfc
--- /dev/null
+++ b/frontend/src/lib/components/PersonHoverCard.svelte.spec.ts
@@ -0,0 +1,348 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import PersonHoverCard from './PersonHoverCard.svelte';
+import type { components } from '$lib/generated/api';
+
+type Person = components['schemas']['Person'];
+type RelationshipDTO = components['schemas']['RelationshipDTO'];
+
+const AUGUSTE: Person = {
+ id: 'p-aug',
+ firstName: 'Auguste',
+ lastName: 'Raddatz',
+ displayName: 'Auguste Raddatz',
+ personType: 'PERSON',
+ familyMember: true,
+ birthYear: 1882,
+ deathYear: 1944
+} as unknown as Person;
+
+const POSITION = { top: 100, left: 200 };
+
+afterEach(() => cleanup());
+
+describe('PersonHoverCard — loading state', () => {
+ it('shows the skeleton when state.status is loading', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ await expect.element(page.getByTestId('person-hover-card-skeleton')).toBeInTheDocument();
+ });
+
+ it('renders three skeleton bars', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ const bars = document.querySelectorAll('[data-testid="person-hover-card-skeleton"] .bar');
+ expect(bars.length).toBe(3);
+ });
+});
+
+describe('PersonHoverCard — error state', () => {
+ it('shows a generic error message when state.status is error', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'error' }
+ });
+ await expect.element(page.getByTestId('person-hover-card-error')).toBeInTheDocument();
+ });
+
+ it('still allows the link footer to navigate (link present in error state)', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'error' }
+ });
+ // The card root must show the footer link even when the body errored —
+ // click navigation works regardless of fetch outcome.
+ const link = document.querySelector('a[href="/persons/p-aug"]');
+ expect(link).not.toBeNull();
+ });
+});
+
+describe('PersonHoverCard — loaded state', () => {
+ it('renders the person displayName as the header name', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument();
+ });
+
+ it('renders the life-date range when birthYear and deathYear are present', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ await expect.element(page.getByText('* 1882 – † 1944')).toBeInTheDocument();
+ });
+
+ it('omits the life-date line when both years are missing', async () => {
+ const noDates = { ...AUGUSTE, birthYear: undefined, deathYear: undefined } as Person;
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: noDates, relationships: [] }
+ });
+ const dates = document.querySelector('[data-testid="person-hover-card-dates"]');
+ expect(dates).toBeNull();
+ });
+
+ it('renders "geb. " when alias is set', async () => {
+ const withAlias = { ...AUGUSTE, alias: 'Müller' } as Person;
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: withAlias, relationships: [] }
+ });
+ await expect.element(page.getByText('geb. Müller')).toBeInTheDocument();
+ });
+
+ it('omits the maiden name line when alias is null', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ const maiden = document.querySelector('[data-testid="person-hover-card-maiden"]');
+ expect(maiden).toBeNull();
+ });
+
+ it('renders family relationship chips for PARENT_OF, SPOUSE_OF, SIBLING_OF only', async () => {
+ const relationships: RelationshipDTO[] = [
+ {
+ id: 'r1',
+ personId: 'p-aug',
+ relatedPersonId: 'p-spouse',
+ personDisplayName: 'Auguste',
+ relatedPersonDisplayName: 'Otto Raddatz',
+ relationType: 'SPOUSE_OF'
+ },
+ {
+ id: 'r2',
+ personId: 'p-aug',
+ relatedPersonId: 'p-friend',
+ personDisplayName: 'Auguste',
+ relatedPersonDisplayName: 'Karl Friend',
+ relationType: 'FRIEND'
+ },
+ {
+ id: 'r3',
+ personId: 'p-aug',
+ relatedPersonId: 'p-sibling',
+ personDisplayName: 'Auguste',
+ relatedPersonDisplayName: 'Marie Sister',
+ relationType: 'SIBLING_OF'
+ }
+ ];
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships }
+ });
+ await expect.element(page.getByText('Otto Raddatz')).toBeInTheDocument();
+ await expect.element(page.getByText('Marie Sister')).toBeInTheDocument();
+ // Non-family relationship type must be filtered out
+ const friendChip = page.getByText('Karl Friend');
+ await expect.element(friendChip).not.toBeInTheDocument();
+ });
+
+ it('omits the chips section entirely when no family relationships', async () => {
+ const onlyFriend: RelationshipDTO[] = [
+ {
+ id: 'r1',
+ personId: 'p-aug',
+ relatedPersonId: 'p-friend',
+ personDisplayName: 'Auguste',
+ relatedPersonDisplayName: 'Karl Friend',
+ relationType: 'FRIEND'
+ }
+ ];
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: onlyFriend }
+ });
+ const chips = document.querySelector('[data-testid="person-hover-card-chips"]');
+ expect(chips).toBeNull();
+ });
+
+ it('renders notes excerpt unchanged when notes ≤ 120 characters', async () => {
+ const withNotes = { ...AUGUSTE, notes: 'Born in Berlin.' } as Person;
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: withNotes, relationships: [] }
+ });
+ await expect.element(page.getByText('Born in Berlin.')).toBeInTheDocument();
+ });
+
+ it('truncates notes longer than 120 characters with an ellipsis (single long word)', async () => {
+ // Single 150-char word with no spaces: word-boundary cut would yield nothing,
+ // so fall back to a hard cut at 120 + ellipsis (Sara #7: pin the exact length).
+ const long = 'x'.repeat(150);
+ const withLongNotes = { ...AUGUSTE, notes: long } as Person;
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: withLongNotes, relationships: [] }
+ });
+ const notes = document.querySelector('[data-testid="person-hover-card-notes"]')!;
+ expect(notes.textContent).toBe('x'.repeat(120) + '…');
+ });
+
+ it('truncates at the last word boundary inside the 120-char window (Leonie FINDING-04)', async () => {
+ // 150-char string with spaces — must cut at the last space, not mid-word.
+ const sentence = 'Sie war eine bekannte Schriftstellerin und engagierte sich '.repeat(3);
+ // length is 180, last space at idx ≤120
+ const withLongNotes = { ...AUGUSTE, notes: sentence } as Person;
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: withLongNotes, relationships: [] }
+ });
+ const notes = document.querySelector('[data-testid="person-hover-card-notes"]')!;
+ const text = notes.textContent ?? '';
+ // Ends with ellipsis
+ expect(text.endsWith('…')).toBe(true);
+ // Last char before the ellipsis is NOT a half-word — verify by checking that
+ // the position right before … is the end of a word (i.e., there's no letter
+ // further along in the original text immediately after our cut point).
+ const cut = text.slice(0, -1); // strip the …
+ // Find this cut substring in the original sentence
+ const idx = sentence.indexOf(cut);
+ expect(idx).toBe(0);
+ const charAfterCut = sentence[cut.length];
+ // The next char should be a space — confirming we cut on a boundary
+ expect(charAfterCut).toBe(' ');
+ });
+
+ it('omits notes section when notes is null', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ const notes = document.querySelector('[data-testid="person-hover-card-notes"]');
+ expect(notes).toBeNull();
+ });
+
+ it('footer renders an anchor link to /persons/{personId}', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ const link = document.querySelector('a[href="/persons/p-aug"]')!;
+ expect(link).not.toBeNull();
+ });
+});
+
+describe('PersonHoverCard — accessibility', () => {
+ it('uses aria-live="polite" so screen readers announce loaded content', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ expect(root.getAttribute('aria-live')).toBe('polite');
+ });
+
+ it('sets aria-busy="true" while loading so SR announces the state change on load', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ expect(root.getAttribute('aria-busy')).toBe('true');
+ });
+
+ it('does not set aria-busy when loaded (so the loaded content is announced)', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ // aria-busy is either absent or "false"
+ const busy = root.getAttribute('aria-busy');
+ expect(busy === null || busy === 'false').toBe(true);
+ });
+
+ it('names the region with the person displayName when loaded (WCAG 1.3.1)', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loaded', person: AUGUSTE, relationships: [] }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ expect(root.getAttribute('aria-label')).toBe('Auguste Raddatz');
+ });
+
+ it('names the region with a generic loading label while loading', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ // Region must have an accessible name in every state — axe-core flags
+ // role="region" without aria-label / aria-labelledby.
+ expect(root.getAttribute('aria-label')).toBeTruthy();
+ });
+
+ it('exposes the cardId as the host element id (so anchor aria-describedby works)', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-xyz',
+ position: POSITION,
+ state: { status: 'loading' }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]')!;
+ expect(root.id).toBe('card-xyz');
+ });
+
+ it('positions itself absolutely at the given top/left', async () => {
+ render(PersonHoverCard, {
+ personId: 'p-aug',
+ cardId: 'card-1',
+ position: { top: 333, left: 444 },
+ state: { status: 'loading' }
+ });
+ const root = document.querySelector('[data-testid="person-hover-card"]') as HTMLElement;
+ expect(root.style.top).toBe('333px');
+ expect(root.style.left).toBe('444px');
+ expect(root.style.position).toBe('absolute');
+ });
+});
diff --git a/frontend/src/lib/components/TranscriptionReadView.svelte b/frontend/src/lib/components/TranscriptionReadView.svelte
index f1586e6c..4a3ac949 100644
--- a/frontend/src/lib/components/TranscriptionReadView.svelte
+++ b/frontend/src/lib/components/TranscriptionReadView.svelte
@@ -1,6 +1,20 @@
-
+
{#each sorted as block (block.id)}
a.sortOrder - b.sortOrder));
onclick={() => onParagraphClick(block.annotationId)}
role="button"
tabindex="0"
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') onParagraphClick(block.annotationId); }}
+ onkeydown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') onParagraphClick(block.annotationId);
+ }}
>
- {#each splitByMarkers(block.text) as segment, i (i)}
- {#if segment.type === 'marker'}
- {segment.text}
- {:else}
- {segment.text}
- {/if}
- {/each}
+
+ {@html renderBlockHtml(block)}
{/each}
+{#if activeCard}
+
+{/if}
+