Compare commits

...

10 Commits

Author SHA1 Message Date
Marcel
b5ec4ebc0c refactor(ui): rename shadowed m parameter to newMode
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 3s
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 2s
Avoids shadowing the Paraglide m import in the onModeChange callback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:58:54 +02:00
Marcel
10fdaf7d00 refactor(ui): use CSS variable for turquoise in flash animations
Replaces hardcoded rgba(0,199,177,...) with color-mix using
var(--color-turquoise) for dark mode compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:58:04 +02:00
Marcel
e01ef56c48 fix(i18n): use getLocale() for date formatting in panel header
Replaces hardcoded 'de-DE' with the active Paraglide locale so
dates render in the user's language.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:56:23 +02:00
Marcel
b01a9ef406 refactor(ui): use bg-turquoise/10 token for paragraph hover
Replaces hardcoded rgba value with the project's turquoise color
token for dark mode compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:54:57 +02:00
Marcel
e31b73303e fix(ui): bump paragraph hover opacity from 6% to 10%
Improves visibility of the clickability affordance on uncalibrated
displays and for senior users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:53:17 +02:00
Marcel
9d9d19ceb5 fix(a11y): increase segmented toggle height on mobile to 36px
Uses h-9 (36px) on mobile, h-7 (28px) on desktop for better tap
targets on small screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:51:56 +02:00
Marcel
0a5c82cd0e fix(a11y): increase panel close button touch target to 44px
Changes h-8 w-8 (32px) to h-11 w-11 (44px) to meet project's
minimum touch target standard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:50:32 +02:00
Marcel
1b063d4e4b test(ui): add tests for 0 blocks and lastEditedAt on PanelHeader
Verifies blockCount=0 shows "0 Abschnitte" and that a provided
lastEditedAt value renders a formatted date containing the year.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:46:53 +02:00
Marcel
b312878b3f test(ui): add annotation-flash class tests for AnnotationLayer
Verifies flashAnnotationId applies and removes the annotation-flash
CSS class correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:45:23 +02:00
Marcel
90120ca8e8 test(ui): add flash-highlight class tests for TranscriptionReadView
Verifies highlightBlockId applies and removes the flash-highlight
CSS class correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:44:01 +02:00
7 changed files with 104 additions and 10 deletions

View File

@@ -182,11 +182,11 @@ const containerStyle = $derived(
<style>
@keyframes annotation-flash-anim {
0% {
outline: 3px solid rgba(0, 199, 177, 0.8);
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
outline-offset: 0px;
}
100% {
outline: 3px solid rgba(0, 199, 177, 0);
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 0%, transparent);
outline-offset: 2px;
}
}
@@ -198,7 +198,7 @@ const containerStyle = $derived(
@media (prefers-reduced-motion: reduce) {
.annotation-flash {
animation: none;
outline: 3px solid rgba(0, 199, 177, 0.8);
outline: 3px solid color-mix(in srgb, var(--color-turquoise) 80%, transparent);
}
}
</style>

View File

@@ -64,4 +64,32 @@ describe('AnnotationLayer', () => {
expect(clickedId).toBe('ann-1');
});
});
describe('flashAnnotationId prop', () => {
it('should apply annotation-flash class when flashAnnotationId matches', async () => {
render(AnnotationLayer, {
annotations: [annotation],
canDraw: false,
color: '#00c7b1',
flashAnnotationId: 'ann-1',
onDraw: () => {}
});
const el = document.querySelector('[data-testid="annotation-ann-1"]')!;
expect(el.classList.contains('annotation-flash')).toBe(true);
});
it('should not apply annotation-flash class when flashAnnotationId does not match', async () => {
render(AnnotationLayer, {
annotations: [annotation],
canDraw: false,
color: '#00c7b1',
flashAnnotationId: 'other-id',
onDraw: () => {}
});
const el = document.querySelector('[data-testid="annotation-ann-1"]')!;
expect(el.classList.contains('annotation-flash')).toBe(false);
});
});
});

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
type Props = {
mode: 'read' | 'edit';
@@ -14,7 +15,7 @@ let { mode, hasBlocks, blockCount, lastEditedAt, onModeChange, onClose }: Props
const formattedDate = $derived(
lastEditedAt
? new Intl.DateTimeFormat('de-DE', {
? new Intl.DateTimeFormat(getLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric'
@@ -33,7 +34,7 @@ function handleReadClick() {
class="flex h-[44px] items-center justify-between border-b border-line bg-surface px-3 font-sans"
>
<!-- Segmented toggle -->
<div class="flex h-7 items-center rounded-full border border-line bg-muted p-0.5">
<div class="flex h-9 items-center rounded-full border border-line bg-muted p-0.5 md:h-7">
<button
type="button"
data-testid="mode-read"
@@ -79,7 +80,7 @@ function handleReadClick() {
data-testid="panel-close"
onclick={onClose}
aria-label={m.transcription_panel_close()}
class="flex h-8 w-8 items-center justify-center rounded text-ink-2 transition-colors hover:bg-muted hover:text-ink"
class="flex h-11 w-11 items-center justify-center rounded text-ink-2 transition-colors hover:bg-muted hover:text-ink"
>
<svg
class="h-4 w-4"

View File

@@ -105,4 +105,47 @@ describe('TranscriptionPanelHeader', () => {
await expect.element(page.getByText('5 Abschnitte')).toBeInTheDocument();
});
it('should show "0 Abschnitte" when blockCount is 0', async () => {
render(TranscriptionPanelHeader, {
mode: 'edit',
hasBlocks: false,
blockCount: 0,
lastEditedAt: null,
onModeChange: () => {},
onClose: () => {}
});
await expect.element(page.getByText('0 Abschnitte')).toBeInTheDocument();
});
it('should have close button with 44px touch target classes', async () => {
render(TranscriptionPanelHeader, {
mode: 'read',
hasBlocks: true,
blockCount: 3,
lastEditedAt: null,
onModeChange: () => {},
onClose: () => {}
});
const closeBtn = document.querySelector('[data-testid="panel-close"]') as HTMLElement;
expect(closeBtn.classList.contains('h-11')).toBe(true);
expect(closeBtn.classList.contains('w-11')).toBe(true);
});
it('should show formatted date when lastEditedAt is provided', async () => {
render(TranscriptionPanelHeader, {
mode: 'read',
hasBlocks: true,
blockCount: 3,
lastEditedAt: '2026-04-07T10:00:00Z',
onModeChange: () => {},
onClose: () => {}
});
const statusText = document.querySelector('.hidden.md\\:block');
expect(statusText).not.toBeNull();
expect(statusText!.textContent).toContain('2026');
});
});

View File

@@ -16,7 +16,7 @@ let sorted = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder));
<article class="px-6 py-8">
{#each sorted as block (block.id)}
<div
class="-mx-2 mb-6 cursor-pointer rounded-sm px-2 py-1 font-serif text-[16px] leading-[1.85] text-ink transition-colors hover:bg-[rgba(0,199,177,0.06)]"
class="-mx-2 mb-6 cursor-pointer rounded-sm px-2 py-1 font-serif text-[16px] leading-[1.85] text-ink transition-colors hover:bg-turquoise/10"
class:flash-highlight={highlightBlockId === block.id}
data-block-id={block.id}
onclick={() => onParagraphClick(block.annotationId)}
@@ -38,7 +38,7 @@ let sorted = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder));
<style>
@keyframes flash {
0% {
background-color: rgba(0, 199, 177, 0.18);
background-color: color-mix(in srgb, var(--color-turquoise) 18%, transparent);
}
100% {
background-color: transparent;
@@ -52,7 +52,7 @@ let sorted = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder));
@media (prefers-reduced-motion: reduce) {
.flash-highlight {
animation: none;
background-color: rgba(0, 199, 177, 0.18);
background-color: color-mix(in srgb, var(--color-turquoise) 18%, transparent);
}
}
</style>

View File

@@ -108,6 +108,28 @@ describe('TranscriptionReadView', () => {
expect(paragraphs[1].getAttribute('data-block-id')).toBe('b1');
});
it('should apply flash-highlight class when highlightBlockId matches', async () => {
render(TranscriptionReadView, {
blocks: [blocks[0]],
onParagraphClick: () => {},
highlightBlockId: 'b1'
});
const el = document.querySelector('[data-block-id="b1"]')!;
expect(el.classList.contains('flash-highlight')).toBe(true);
});
it('should not apply flash-highlight class when highlightBlockId does not match', async () => {
render(TranscriptionReadView, {
blocks: [blocks[0]],
onParagraphClick: () => {},
highlightBlockId: 'other-id'
});
const el = document.querySelector('[data-block-id="b1"]')!;
expect(el.classList.contains('flash-highlight')).toBe(false);
});
it('should render empty state when no blocks', async () => {
render(TranscriptionReadView, {
blocks: [],

View File

@@ -299,7 +299,7 @@ onMount(() => {
hasBlocks={hasBlocks}
blockCount={transcriptionBlocks.length}
lastEditedAt={lastEditedAt}
onModeChange={(m) => (panelMode = m)}
onModeChange={(newMode) => (panelMode = newMode)}
onClose={() => (transcribeMode = false)}
/>
<div class="flex-1 overflow-y-auto">