Compare commits

..

3 Commits

Author SHA1 Message Date
Marcel
35c2c83996 test(nav): add ThemeToggle spec covering label derivation in both modes
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m0s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Failing after 2m56s
CI / Unit & Component Tests (pull_request) Failing after 3m1s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Failing after 2m56s
Covers the concern raised during PR review: themeLabel $derived logic
had no tests. Adds 4 tests — aria-label and title=aria-label assertions
in light mode and dark mode — mirroring the NotificationBell pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:27:03 +02:00
Marcel
c317c085aa fix(nav): replace hardcoded ThemeToggle title with Paraglide i18n keys
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m13s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m13s
CI / Unit & Component Tests (pull_request) Failing after 2m59s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Failing after 2m56s
Add theme_toggle_to_light / theme_toggle_to_dark to de/en/es messages.
Extract themeLabel as $derived and use it for both aria-label and title,
matching the pattern applied to NotificationBell.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 20:53:10 +02:00
Marcel
bc805cb178 feat(nav): add cursor-pointer and tooltip to notification bell
Extract bellLabel as $derived to DRY up aria-label and title.
Add cursor-pointer globally to button via @layer base so Tailwind
preflight reset doesn't override the browser default.

Closes #344

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 20:46:12 +02:00
16 changed files with 104 additions and 268 deletions

View File

@@ -260,13 +260,6 @@ class TranscriptionBlockControllerTest {
.andExpect(status().isForbidden()); .andExpect(status().isForbidden());
} }
@Test
@WithMockUser(authorities = "READ_ALL")
void deleteBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception {
mockMvc.perform(delete(URL_BLOCK))
.andExpect(status().isForbidden());
}
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void deleteBlock_returns204_whenAuthorised() throws Exception { void deleteBlock_returns204_whenAuthorised() throws Exception {

View File

@@ -23,6 +23,8 @@
"nav_conversations": "Briefwechsel", "nav_conversations": "Briefwechsel",
"nav_admin": "Admin", "nav_admin": "Admin",
"nav_logout": "Abmelden", "nav_logout": "Abmelden",
"theme_toggle_to_light": "Zu hellem Design wechseln",
"theme_toggle_to_dark": "Zu dunklem Design wechseln",
"btn_save": "Speichern", "btn_save": "Speichern",
"btn_cancel": "Abbrechen", "btn_cancel": "Abbrechen",
"btn_confirm": "Bestätigen", "btn_confirm": "Bestätigen",

View File

@@ -23,6 +23,8 @@
"nav_conversations": "Letters", "nav_conversations": "Letters",
"nav_admin": "Admin", "nav_admin": "Admin",
"nav_logout": "Sign out", "nav_logout": "Sign out",
"theme_toggle_to_light": "Switch to light mode",
"theme_toggle_to_dark": "Switch to dark mode",
"btn_save": "Save", "btn_save": "Save",
"btn_cancel": "Cancel", "btn_cancel": "Cancel",
"btn_confirm": "Confirm", "btn_confirm": "Confirm",

View File

@@ -23,6 +23,8 @@
"nav_conversations": "Cartas", "nav_conversations": "Cartas",
"nav_admin": "Admin", "nav_admin": "Admin",
"nav_logout": "Cerrar sesión", "nav_logout": "Cerrar sesión",
"theme_toggle_to_light": "Cambiar a modo claro",
"theme_toggle_to_dark": "Cambiar a modo oscuro",
"btn_save": "Guardar", "btn_save": "Guardar",
"btn_cancel": "Cancelar", "btn_cancel": "Cancelar",
"btn_confirm": "Confirmar", "btn_confirm": "Confirmar",

View File

@@ -18,8 +18,7 @@ let {
dimmed = false, dimmed = false,
flashAnnotationId = null, flashAnnotationId = null,
onDraw, onDraw,
onAnnotationClick, onAnnotationClick
onDeleteRequest
}: { }: {
annotations: Annotation[]; annotations: Annotation[];
canDraw: boolean; canDraw: boolean;
@@ -30,7 +29,6 @@ let {
flashAnnotationId?: string | null; flashAnnotationId?: string | null;
onDraw: (rect: DrawRect) => void; onDraw: (rect: DrawRect) => void;
onAnnotationClick?: (id: string) => void; onAnnotationClick?: (id: string) => void;
onDeleteRequest?: (annotationId: string) => void;
} = $props(); } = $props();
let drawStart = $state<{ x: number; y: number } | null>(null); let drawStart = $state<{ x: number; y: number } | null>(null);
@@ -114,8 +112,6 @@ const containerStyle = $derived(
dimmed={dimmed} dimmed={dimmed}
blockNumber={blockNumbers[annotation.id]} blockNumber={blockNumbers[annotation.id]}
isFlashing={flashAnnotationId === annotation.id} isFlashing={flashAnnotationId === annotation.id}
showDelete={canDraw}
onDeleteRequest={() => onDeleteRequest?.(annotation.id)}
onclick={() => onAnnotationClick?.(annotation.id)} onclick={() => onAnnotationClick?.(annotation.id)}
onpointerenter={() => (hoveredId = annotation.id)} onpointerenter={() => (hoveredId = annotation.id)}
onpointerleave={() => (hoveredId = null)} onpointerleave={() => (hoveredId = null)}

View File

@@ -98,7 +98,7 @@ describe('AnnotationLayer', () => {
expect(el2.style.opacity).toBe('1'); expect(el2.style.opacity).toBe('1');
}); });
it('does not show delete button when annotation is not hovered or active', async () => { it('does not show delete buttons (annotations owned by blocks)', async () => {
render(AnnotationLayer, { render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')], annotations: [makeAnnotation('ann-1')],
canDraw: true, canDraw: true,
@@ -107,19 +107,6 @@ describe('AnnotationLayer', () => {
}); });
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument(); await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull(); expect(page.getByRole('button', { name: /löschen/i }).query()).toBeNull();
});
it('does not show delete button when canDraw is false even if annotation is active', async () => {
render(AnnotationLayer, {
annotations: [makeAnnotation('ann-1')],
canDraw: false,
color: '#00C7B1',
activeAnnotationId: 'ann-1',
onDraw: () => {}
});
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
}); });
}); });

View File

@@ -11,8 +11,6 @@ let {
blockNumber = undefined, blockNumber = undefined,
isFlashing = false, isFlashing = false,
isResizable = false, isResizable = false,
showDelete = false,
onDeleteRequest,
onclick, onclick,
onpointerenter, onpointerenter,
onpointerleave onpointerleave
@@ -25,15 +23,11 @@ let {
blockNumber?: number | undefined; blockNumber?: number | undefined;
isFlashing?: boolean; isFlashing?: boolean;
isResizable?: boolean; isResizable?: boolean;
showDelete?: boolean;
onDeleteRequest?: () => void;
onclick: () => void; onclick: () => void;
onpointerenter: () => void; onpointerenter: () => void;
onpointerleave: () => void; onpointerleave: () => void;
} = $props(); } = $props();
const deleteVisible = $derived(showDelete && (isHovered || isActive));
function hexToRgba(hex: string, alpha: number): string { function hexToRgba(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16); const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16); const g = parseInt(hex.slice(3, 5), 16);
@@ -89,7 +83,6 @@ let shapeStyle = $derived(
onclick={onclick} onclick={onclick}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onclick(); if (e.key === 'Enter' || e.key === ' ') onclick();
if (e.key === 'Delete' && showDelete) onDeleteRequest?.();
}} }}
onpointerenter={onpointerenter} onpointerenter={onpointerenter}
onpointerleave={onpointerleave} onpointerleave={onpointerleave}
@@ -119,51 +112,6 @@ let shapeStyle = $derived(
{blockNumber} {blockNumber}
</div> </div>
{/if} {/if}
{#if deleteVisible}
<button
data-testid="annotation-delete-{annotation.id}"
type="button"
aria-label="Löschen"
onclick={(e) => {
e.stopPropagation();
onDeleteRequest?.();
}}
style="
position: absolute;
top: -8px;
right: -8px;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff;
border: 1px solid var(--color-error, #e53e3e);
color: var(--color-error, #e53e3e);
cursor: pointer;
pointer-events: auto;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
z-index: 10;
"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
{/if}
{#if isResizable} {#if isResizable}
<AnnotationEditOverlay annotation={annotation} /> <AnnotationEditOverlay annotation={annotation} />
{/if} {/if}

View File

@@ -1,155 +0,0 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import AnnotationShape from './AnnotationShape.svelte';
afterEach(cleanup);
function makeAnnotation(id = 'ann-1') {
return {
id,
documentId: 'doc-1',
pageNumber: 1,
x: 0.1,
y: 0.1,
width: 0.3,
height: 0.2,
color: '#00C7B1',
createdAt: new Date().toISOString()
};
}
describe('AnnotationShape', () => {
it('renders the annotation element', async () => {
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: false,
isActive: false,
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
});
it('does not show delete button when showDelete is false', async () => {
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: true,
isActive: false,
showDelete: false,
onDeleteRequest: vi.fn(),
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
});
it('does not show delete button when showDelete is true but neither hovered nor active', async () => {
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: false,
isActive: false,
showDelete: true,
onDeleteRequest: vi.fn(),
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
});
it('shows delete button when showDelete is true and isHovered is true', async () => {
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: true,
isActive: false,
showDelete: true,
onDeleteRequest: vi.fn(),
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
await expect.element(page.getByTestId('annotation-delete-ann-1')).toBeInTheDocument();
});
it('shows delete button when showDelete is true and isActive is true', async () => {
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: false,
isActive: true,
showDelete: true,
onDeleteRequest: vi.fn(),
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
await expect.element(page.getByTestId('annotation-delete-ann-1')).toBeInTheDocument();
});
it('calls onDeleteRequest when delete button is clicked', async () => {
const onDeleteRequest = vi.fn();
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: true,
isActive: false,
showDelete: true,
onDeleteRequest,
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
const deleteBtn = page.getByTestId('annotation-delete-ann-1');
await deleteBtn.click();
expect(onDeleteRequest).toHaveBeenCalledOnce();
});
it('calls onDeleteRequest when Delete key is pressed on the annotation', async () => {
const onDeleteRequest = vi.fn();
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: false,
isActive: true,
showDelete: true,
onDeleteRequest,
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
annotationEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
expect(onDeleteRequest).toHaveBeenCalledOnce();
});
it('does not call onDeleteRequest on Delete key when showDelete is false', async () => {
const onDeleteRequest = vi.fn();
render(AnnotationShape, {
annotation: makeAnnotation(),
isHovered: false,
isActive: true,
showDelete: false,
onDeleteRequest,
onclick: () => {},
onpointerenter: () => {},
onpointerleave: () => {}
});
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
annotationEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
expect(onDeleteRequest).not.toHaveBeenCalled();
});
});

View File

@@ -24,7 +24,6 @@ type Props = {
flashAnnotationId?: string | null; flashAnnotationId?: string | null;
onAnnotationClick: (id: string) => void; onAnnotationClick: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void; onTranscriptionDraw?: (rect: DrawRect) => void;
onDeleteAnnotationRequest?: (annotationId: string) => void;
}; };
let { let {
@@ -39,8 +38,7 @@ let {
annotationsDimmed = false, annotationsDimmed = false,
flashAnnotationId = null, flashAnnotationId = null,
onAnnotationClick, onAnnotationClick,
onTranscriptionDraw, onTranscriptionDraw
onDeleteAnnotationRequest
}: Props = $props(); }: Props = $props();
</script> </script>
@@ -100,7 +98,6 @@ let {
flashAnnotationId={flashAnnotationId} flashAnnotationId={flashAnnotationId}
onAnnotationClick={onAnnotationClick} onAnnotationClick={onAnnotationClick}
onTranscriptionDraw={onTranscriptionDraw} onTranscriptionDraw={onTranscriptionDraw}
onDeleteAnnotationRequest={onDeleteAnnotationRequest}
documentFileHash={doc.fileHash ?? null} documentFileHash={doc.fileHash ?? null}
/> />
{:else if fileUrl} {:else if fileUrl}

View File

@@ -48,6 +48,12 @@ function handleKeydown(event: KeyboardEvent) {
} }
} }
const bellLabel = $derived(
stream.unreadCount > 0
? m.notification_bell_unread_label({ count: stream.unreadCount })
: m.notification_bell_label()
);
function attachBellButton(node: HTMLButtonElement) { function attachBellButton(node: HTMLButtonElement) {
bellButtonEl = node; bellButtonEl = node;
return () => { return () => {
@@ -72,12 +78,11 @@ onDestroy(() => {
{@attach attachBellButton} {@attach attachBellButton}
type="button" type="button"
onclick={toggleDropdown} onclick={toggleDropdown}
aria-label={stream.unreadCount > 0 aria-label={bellLabel}
? m.notification_bell_unread_label({ count: stream.unreadCount }) title={bellLabel}
: m.notification_bell_label()}
aria-expanded={open} aria-expanded={open}
aria-haspopup="true" aria-haspopup="true"
class="relative rounded-sm p-2 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" class="relative cursor-pointer rounded-sm p-2 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -55,6 +55,34 @@ async function openDropdownAndClickFirstNotification() {
notifButton.click(); notifButton.click();
} }
describe('NotificationBell — cursor and tooltip', () => {
it('bell button has cursor-pointer class', async () => {
render(NotificationBell);
const btn = document.querySelector<HTMLButtonElement>('button[aria-haspopup="true"]')!;
expect(btn.classList.contains('cursor-pointer')).toBe(true);
});
it('bell button title equals aria-label when unreadCount is 0', async () => {
mockNotificationList.value = [];
render(NotificationBell);
const btn = document.querySelector<HTMLButtonElement>('button[aria-haspopup="true"]')!;
expect(btn.getAttribute('title')).toBe('Benachrichtigungen');
expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
});
it('bell button title equals aria-label when unreadCount is 3', async () => {
mockNotificationList.value = [
makeNotification({ id: 'n1' }),
makeNotification({ id: 'n2' }),
makeNotification({ id: 'n3' })
];
render(NotificationBell);
const btn = document.querySelector<HTMLButtonElement>('button[aria-haspopup="true"]')!;
expect(btn.getAttribute('title')).toBe('3 ungelesene Benachrichtigungen');
expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
});
});
describe('NotificationBell', () => { describe('NotificationBell', () => {
it('handleMarkRead navigates to URL including annotationId when notification has annotationId', async () => { it('handleMarkRead navigates to URL including annotationId when notification has annotationId', async () => {
mockNotificationList.value = [makeNotification({ annotationId: 'annot-1' })]; mockNotificationList.value = [makeNotification({ annotationId: 'annot-1' })];

View File

@@ -18,7 +18,6 @@ let {
activeAnnotationId = $bindable<string | null>(null), activeAnnotationId = $bindable<string | null>(null),
onAnnotationClick, onAnnotationClick,
onTranscriptionDraw, onTranscriptionDraw,
onDeleteAnnotationRequest,
documentFileHash, documentFileHash,
annotationsDimmed = false, annotationsDimmed = false,
flashAnnotationId = null flashAnnotationId = null
@@ -31,7 +30,6 @@ let {
activeAnnotationId?: string | null; activeAnnotationId?: string | null;
onAnnotationClick?: (id: string) => void; onAnnotationClick?: (id: string) => void;
onTranscriptionDraw?: (rect: DrawRect) => void; onTranscriptionDraw?: (rect: DrawRect) => void;
onDeleteAnnotationRequest?: (annotationId: string) => void;
documentFileHash?: string | null; documentFileHash?: string | null;
annotationsDimmed?: boolean; annotationsDimmed?: boolean;
flashAnnotationId?: string | null; flashAnnotationId?: string | null;
@@ -266,7 +264,6 @@ function handleAnnotationClick(id: string) {
flashAnnotationId={flashAnnotationId} flashAnnotationId={flashAnnotationId}
onDraw={handleDraw} onDraw={handleDraw}
onAnnotationClick={handleAnnotationClick} onAnnotationClick={handleAnnotationClick}
onDeleteRequest={onDeleteAnnotationRequest}
/> />
{/if} {/if}
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
type Theme = 'light' | 'dark'; type Theme = 'light' | 'dark';
@@ -19,6 +20,10 @@ onMount(() => {
theme = resolveInitialTheme(); theme = resolveInitialTheme();
}); });
const themeLabel = $derived(
theme === 'dark' ? m.theme_toggle_to_light() : m.theme_toggle_to_dark()
);
function toggle() { function toggle() {
theme = theme === 'dark' ? 'light' : 'dark'; theme = theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
@@ -29,8 +34,8 @@ function toggle() {
<button <button
type="button" type="button"
onclick={toggle} onclick={toggle}
aria-label={theme === 'dark' ? 'light mode' : 'dark mode'} aria-label={themeLabel}
title={theme === 'dark' ? 'light mode' : 'dark mode'} title={themeLabel}
class="rounded p-1.5 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" class="rounded p-1.5 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
> >
{#if theme === 'dark'} {#if theme === 'dark'}

View File

@@ -0,0 +1,45 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import ThemeToggle from './ThemeToggle.svelte';
afterEach(() => {
cleanup();
localStorage.removeItem('theme');
});
describe('ThemeToggle — label derivation (light mode)', () => {
beforeEach(() => {
localStorage.setItem('theme', 'light');
});
it('aria-label invites switching to dark mode when theme is light', async () => {
render(ThemeToggle);
const btn = await page.getByRole('button').element();
expect(btn.getAttribute('aria-label')).toBe('Zu dunklem Design wechseln');
});
it('title equals aria-label in light mode', async () => {
render(ThemeToggle);
const btn = await page.getByRole('button').element();
expect(btn.getAttribute('title')).toBe(btn.getAttribute('aria-label'));
});
});
describe('ThemeToggle — label derivation (dark mode)', () => {
beforeEach(() => {
localStorage.setItem('theme', 'dark');
});
it('aria-label invites switching to light mode when theme is dark', async () => {
render(ThemeToggle);
const btn = await page.getByRole('button').element();
expect(btn.getAttribute('aria-label')).toBe('Zu hellem Design wechseln');
});
it('title equals aria-label in dark mode', async () => {
render(ThemeToggle);
const btn = await page.getByRole('button').element();
expect(btn.getAttribute('title')).toBe(btn.getAttribute('aria-label'));
});
});

View File

@@ -13,12 +13,9 @@ import { getErrorMessage } from '$lib/errors';
import { translateOcrProgress } from '$lib/ocr/translateOcrProgress'; import { translateOcrProgress } from '$lib/ocr/translateOcrProgress';
import { createFileLoader } from '$lib/hooks/useFileLoader.svelte'; import { createFileLoader } from '$lib/hooks/useFileLoader.svelte';
import { scrollToCommentFromQuery } from '$lib/utils/deepLinkScroll'; import { scrollToCommentFromQuery } from '$lib/utils/deepLinkScroll';
import { getConfirmService } from '$lib/services/confirm.svelte.js';
let { data } = $props(); let { data } = $props();
const { confirm } = getConfirmService();
const doc = $derived(data.document); const doc = $derived(data.document);
const canWrite = $derived(data.canWrite ?? false); const canWrite = $derived(data.canWrite ?? false);
const currentUserId = $derived((data.user?.id as string | undefined) ?? null); const currentUserId = $derived((data.user?.id as string | undefined) ?? null);
@@ -108,23 +105,6 @@ async function deleteBlock(blockId: string) {
annotationReloadKey++; annotationReloadKey++;
} }
async function handleAnnotationDeleteRequest(annotationId: string) {
const confirmed = await confirm({
title: m.transcription_block_delete_confirm(),
destructive: true
});
if (!confirmed) return;
const block = transcriptionBlocks.find((b) => b.annotationId === annotationId);
if (block) {
await deleteBlock(block.id);
} else {
// Annotation has no linked block — delete the annotation directly
await fetch(`/api/documents/${doc.id}/annotations/${annotationId}`, { method: 'DELETE' });
annotationReloadKey++;
}
}
async function reviewToggle(blockId: string) { async function reviewToggle(blockId: string) {
const res = await fetch(`/api/documents/${doc.id}/transcription-blocks/${blockId}/review`, { const res = await fetch(`/api/documents/${doc.id}/transcription-blocks/${blockId}/review`, {
method: 'PUT' method: 'PUT'
@@ -401,7 +381,6 @@ onMount(() => {
bind:activeAnnotationId={activeAnnotationId} bind:activeAnnotationId={activeAnnotationId}
onAnnotationClick={handleAnnotationClick} onAnnotationClick={handleAnnotationClick}
onTranscriptionDraw={createBlockFromDraw} onTranscriptionDraw={createBlockFromDraw}
onDeleteAnnotationRequest={handleAnnotationDeleteRequest}
/> />
</div> </div>

View File

@@ -365,6 +365,11 @@
text-underline-offset: 4px; text-underline-offset: 4px;
} }
/* Tailwind preflight resets cursor on *, overriding the browser default for buttons */
button {
cursor: pointer;
}
/* Fallback focus ring for any interactive element not styled with ring-focus-ring */ /* Fallback focus ring for any interactive element not styled with ring-focus-ring */
:focus-visible { :focus-visible {
outline: 2px solid var(--c-focus-ring); outline: 2px solid var(--c-focus-ring);