feat(transcribe): wire keyboard shortcuts into the document panel (#327)
Attaches the transcribeShortcuts action to the document page and wires every command to existing context setters: j/k walk the sortOrder-sorted regions and set activeAnnotationId, e toggles read/edit, n arms a draw cue (edit only), Delete routes to the existing confirm path, ? opens the cheatsheet, and Esc is now owned solely by the action — the inline onMount Esc listener is removed (decision B1). Renders ShortcutCheatsheet and a draw-armed hint. "t" toggles the document-level KURRENT_RECOGNITION training enrollment (the only training surface that exists; there is no per-region flag yet — see #321) and no-ops unless a region is active. Also reconciles annotation Delete: the shape no longer self-handles the key, with onfocus syncing the active region so the action deletes exactly once. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ 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;
|
onAnnotationFocus?: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -42,7 +42,7 @@ let {
|
|||||||
flashAnnotationId = null,
|
flashAnnotationId = null,
|
||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
onTranscriptionDraw,
|
onTranscriptionDraw,
|
||||||
onDeleteAnnotationRequest
|
onAnnotationFocus
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ let {
|
|||||||
flashAnnotationId={flashAnnotationId}
|
flashAnnotationId={flashAnnotationId}
|
||||||
onAnnotationClick={onAnnotationClick}
|
onAnnotationClick={onAnnotationClick}
|
||||||
onTranscriptionDraw={onTranscriptionDraw}
|
onTranscriptionDraw={onTranscriptionDraw}
|
||||||
onDeleteAnnotationRequest={onDeleteAnnotationRequest}
|
onAnnotationFocus={onAnnotationFocus}
|
||||||
documentFileHash={doc.fileHash ?? null}
|
documentFileHash={doc.fileHash ?? null}
|
||||||
/>
|
/>
|
||||||
{:else if fileUrl}
|
{:else if fileUrl}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ let {
|
|||||||
flashAnnotationId = null,
|
flashAnnotationId = null,
|
||||||
onDraw,
|
onDraw,
|
||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
onDeleteRequest
|
onAnnotationFocus
|
||||||
}: {
|
}: {
|
||||||
annotations: Annotation[];
|
annotations: Annotation[];
|
||||||
canDraw: boolean;
|
canDraw: boolean;
|
||||||
@@ -30,7 +30,7 @@ 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;
|
onAnnotationFocus?: (id: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let drawStart = $state<{ x: number; y: number } | null>(null);
|
let drawStart = $state<{ x: number; y: number } | null>(null);
|
||||||
@@ -115,8 +115,8 @@ const containerStyle = $derived(
|
|||||||
blockNumber={blockNumbers[annotation.id]}
|
blockNumber={blockNumbers[annotation.id]}
|
||||||
isFlashing={flashAnnotationId === annotation.id}
|
isFlashing={flashAnnotationId === annotation.id}
|
||||||
showDelete={canDraw}
|
showDelete={canDraw}
|
||||||
onDeleteRequest={() => onDeleteRequest?.(annotation.id)}
|
|
||||||
onclick={() => onAnnotationClick?.(annotation.id)}
|
onclick={() => onAnnotationClick?.(annotation.id)}
|
||||||
|
onfocus={() => onAnnotationFocus?.(annotation.id)}
|
||||||
onpointerenter={() => (hoveredId = annotation.id)}
|
onpointerenter={() => (hoveredId = annotation.id)}
|
||||||
onpointerleave={() => (hoveredId = null)}
|
onpointerleave={() => (hoveredId = null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Annotation } from '$lib/shared/types';
|
import type { Annotation } from '$lib/shared/types';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import AnnotationEditOverlay from './AnnotationEditOverlay.svelte';
|
import AnnotationEditOverlay from './AnnotationEditOverlay.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -12,8 +13,8 @@ let {
|
|||||||
isFlashing = false,
|
isFlashing = false,
|
||||||
isResizable = false,
|
isResizable = false,
|
||||||
showDelete = false,
|
showDelete = false,
|
||||||
onDeleteRequest,
|
|
||||||
onclick,
|
onclick,
|
||||||
|
onfocus,
|
||||||
onpointerenter,
|
onpointerenter,
|
||||||
onpointerleave
|
onpointerleave
|
||||||
}: {
|
}: {
|
||||||
@@ -26,12 +27,17 @@ let {
|
|||||||
isFlashing?: boolean;
|
isFlashing?: boolean;
|
||||||
isResizable?: boolean;
|
isResizable?: boolean;
|
||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
onDeleteRequest?: () => void;
|
|
||||||
onclick: () => void;
|
onclick: () => void;
|
||||||
|
onfocus?: () => void;
|
||||||
onpointerenter: () => void;
|
onpointerenter: () => void;
|
||||||
onpointerleave: () => void;
|
onpointerleave: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
// When deletion is available (transcribe mode), announce the otherwise-hidden
|
||||||
|
// Delete affordance to assistive tech (issue #327). The transcribeShortcuts
|
||||||
|
// action is the single owner of the key itself.
|
||||||
|
const ariaLabel = $derived(showDelete ? m.annotation_label_with_delete() : 'Block anzeigen');
|
||||||
|
|
||||||
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);
|
||||||
@@ -83,11 +89,12 @@ let shapeStyle = $derived(
|
|||||||
class:annotation-flash={isFlashing}
|
class:annotation-flash={isFlashing}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Block anzeigen"
|
aria-label={ariaLabel}
|
||||||
|
aria-keyshortcuts={showDelete ? 'Delete' : undefined}
|
||||||
onclick={onclick}
|
onclick={onclick}
|
||||||
|
onfocus={onfocus}
|
||||||
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}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ describe('AnnotationShape', () => {
|
|||||||
isHovered: true,
|
isHovered: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
onDeleteRequest: vi.fn(),
|
|
||||||
onclick: () => {},
|
onclick: () => {},
|
||||||
onpointerenter: () => {},
|
onpointerenter: () => {},
|
||||||
onpointerleave: () => {}
|
onpointerleave: () => {}
|
||||||
@@ -57,16 +56,17 @@ describe('AnnotationShape', () => {
|
|||||||
expect(annotationEl.querySelectorAll('button').length).toBe(0);
|
expect(annotationEl.querySelectorAll('button').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onDeleteRequest when Delete key is pressed on the annotation', async () => {
|
// Deletion is owned solely by the transcribeShortcuts action (issue #327,
|
||||||
const onDeleteRequest = vi.fn();
|
// decision: action is the single Delete owner). The shape must NOT handle
|
||||||
|
// the Delete key itself, or the key would delete twice.
|
||||||
|
it('does not act on the Delete key itself (the action owns deletion)', async () => {
|
||||||
|
const onclick = vi.fn();
|
||||||
render(AnnotationShape, {
|
render(AnnotationShape, {
|
||||||
annotation: makeAnnotation(),
|
annotation: makeAnnotation(),
|
||||||
isHovered: false,
|
isHovered: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
onDeleteRequest,
|
onclick,
|
||||||
onclick: () => {},
|
|
||||||
onpointerenter: () => {},
|
onpointerenter: () => {},
|
||||||
onpointerleave: () => {}
|
onpointerleave: () => {}
|
||||||
});
|
});
|
||||||
@@ -74,26 +74,58 @@ describe('AnnotationShape', () => {
|
|||||||
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
||||||
annotationEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
annotationEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
||||||
|
|
||||||
expect(onDeleteRequest).toHaveBeenCalledOnce();
|
// No side effect from the shape; it stays in the document for the action to act on.
|
||||||
|
expect(onclick).not.toHaveBeenCalled();
|
||||||
|
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call onDeleteRequest on Delete key when showDelete is false', async () => {
|
it('announces the Delete affordance via aria when deletion is available', async () => {
|
||||||
const onDeleteRequest = vi.fn();
|
|
||||||
|
|
||||||
render(AnnotationShape, {
|
render(AnnotationShape, {
|
||||||
annotation: makeAnnotation(),
|
annotation: makeAnnotation(),
|
||||||
isHovered: false,
|
isHovered: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
showDelete: false,
|
showDelete: true,
|
||||||
onDeleteRequest,
|
|
||||||
onclick: () => {},
|
onclick: () => {},
|
||||||
onpointerenter: () => {},
|
onpointerenter: () => {},
|
||||||
onpointerleave: () => {}
|
onpointerleave: () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
||||||
annotationEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
expect(annotationEl.getAttribute('aria-keyshortcuts')).toBe('Delete');
|
||||||
|
expect(annotationEl.getAttribute('aria-label')).toContain('Entf');
|
||||||
|
});
|
||||||
|
|
||||||
expect(onDeleteRequest).not.toHaveBeenCalled();
|
it('keeps the plain label and no key hint when deletion is unavailable', async () => {
|
||||||
|
render(AnnotationShape, {
|
||||||
|
annotation: makeAnnotation(),
|
||||||
|
isHovered: false,
|
||||||
|
isActive: false,
|
||||||
|
showDelete: false,
|
||||||
|
onclick: () => {},
|
||||||
|
onpointerenter: () => {},
|
||||||
|
onpointerleave: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
||||||
|
expect(annotationEl.getAttribute('aria-keyshortcuts')).toBe(null);
|
||||||
|
expect(annotationEl.getAttribute('aria-label')).toBe('Block anzeigen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onfocus when the annotation receives focus', async () => {
|
||||||
|
const onfocus = vi.fn();
|
||||||
|
render(AnnotationShape, {
|
||||||
|
annotation: makeAnnotation(),
|
||||||
|
isHovered: false,
|
||||||
|
isActive: false,
|
||||||
|
onfocus,
|
||||||
|
onclick: () => {},
|
||||||
|
onpointerenter: () => {},
|
||||||
|
onpointerleave: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const annotationEl = page.getByTestId('annotation-ann-1').element() as HTMLElement;
|
||||||
|
annotationEl.dispatchEvent(new FocusEvent('focus'));
|
||||||
|
|
||||||
|
expect(onfocus).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ let {
|
|||||||
activeAnnotationId = $bindable<string | null>(null),
|
activeAnnotationId = $bindable<string | null>(null),
|
||||||
onAnnotationClick,
|
onAnnotationClick,
|
||||||
onTranscriptionDraw,
|
onTranscriptionDraw,
|
||||||
onDeleteAnnotationRequest,
|
onAnnotationFocus,
|
||||||
documentFileHash,
|
documentFileHash,
|
||||||
annotationsDimmed = false,
|
annotationsDimmed = false,
|
||||||
flashAnnotationId = null,
|
flashAnnotationId = null,
|
||||||
@@ -35,7 +35,7 @@ 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;
|
onAnnotationFocus?: (id: string) => void;
|
||||||
documentFileHash?: string | null;
|
documentFileHash?: string | null;
|
||||||
annotationsDimmed?: boolean;
|
annotationsDimmed?: boolean;
|
||||||
flashAnnotationId?: string | null;
|
flashAnnotationId?: string | null;
|
||||||
@@ -294,7 +294,7 @@ function handleAnnotationClick(id: string) {
|
|||||||
flashAnnotationId={flashAnnotationId}
|
flashAnnotationId={flashAnnotationId}
|
||||||
onDraw={handleDraw}
|
onDraw={handleDraw}
|
||||||
onAnnotationClick={handleAnnotationClick}
|
onAnnotationClick={handleAnnotationClick}
|
||||||
onDeleteRequest={onDeleteAnnotationRequest}
|
onAnnotationFocus={onAnnotationFocus}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import DocumentViewer from '$lib/document/DocumentViewer.svelte';
|
|||||||
import TranscriptionEditView from '$lib/document/transcription/TranscriptionEditView.svelte';
|
import TranscriptionEditView from '$lib/document/transcription/TranscriptionEditView.svelte';
|
||||||
import TranscriptionReadView from '$lib/document/transcription/TranscriptionReadView.svelte';
|
import TranscriptionReadView from '$lib/document/transcription/TranscriptionReadView.svelte';
|
||||||
import TranscriptionPanelHeader from '$lib/document/transcription/TranscriptionPanelHeader.svelte';
|
import TranscriptionPanelHeader from '$lib/document/transcription/TranscriptionPanelHeader.svelte';
|
||||||
|
import ShortcutCheatsheet from '$lib/document/transcription/ShortcutCheatsheet.svelte';
|
||||||
|
import { transcribeShortcuts } from '$lib/shared/actions/transcribeShortcuts';
|
||||||
import { createOcrJob } from '$lib/ocr/useOcrJob.svelte';
|
import { createOcrJob } from '$lib/ocr/useOcrJob.svelte';
|
||||||
import { createTranscriptionBlocks } from '$lib/document/transcription/useTranscriptionBlocks.svelte';
|
import { createTranscriptionBlocks } from '$lib/document/transcription/useTranscriptionBlocks.svelte';
|
||||||
import { createFileLoader } from '$lib/document/viewer/useFileLoader.svelte';
|
import { createFileLoader } from '$lib/document/viewer/useFileLoader.svelte';
|
||||||
@@ -42,6 +44,8 @@ let activeAnnotationId = $state<string | null>(null);
|
|||||||
let highlightBlockId = $state<string | null>(null);
|
let highlightBlockId = $state<string | null>(null);
|
||||||
let flashAnnotationId = $state<string | null>(null);
|
let flashAnnotationId = $state<string | null>(null);
|
||||||
let pdfStripExpanded = $state(false);
|
let pdfStripExpanded = $state(false);
|
||||||
|
let cheatsheetOpen = $state(false);
|
||||||
|
let drawArmed = $state(false);
|
||||||
// Flag set by the deep-link helper so the transcribe-mode $effect does not
|
// Flag set by the deep-link helper so the transcribe-mode $effect does not
|
||||||
// overwrite the panelMode it picked (e.g. forcing 'edit' on notification
|
// overwrite the panelMode it picked (e.g. forcing 'edit' on notification
|
||||||
// click-through). One-shot: consumed after the effect's loadBlocks resolves.
|
// click-through). One-shot: consumed after the effect's loadBlocks resolves.
|
||||||
@@ -86,12 +90,69 @@ async function createBlockFromDraw(rect: {
|
|||||||
height: number;
|
height: number;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
}) {
|
}) {
|
||||||
|
drawArmed = false;
|
||||||
const created = await transcription.createFromDraw(rect);
|
const created = await transcription.createFromDraw(rect);
|
||||||
if (created) {
|
if (created) {
|
||||||
activeAnnotationId = created.annotationId;
|
activeAnnotationId = created.annotationId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Keyboard shortcuts (issue #327) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
const sortedBlocks = $derived([...transcription.blocks].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||||
|
|
||||||
|
function goToRegion(delta: 1 | -1) {
|
||||||
|
if (sortedBlocks.length === 0) return;
|
||||||
|
const current = sortedBlocks.findIndex((b) => b.annotationId === activeAnnotationId);
|
||||||
|
const next =
|
||||||
|
current === -1
|
||||||
|
? delta > 0
|
||||||
|
? 0
|
||||||
|
: sortedBlocks.length - 1
|
||||||
|
: (current + delta + sortedBlocks.length) % sortedBlocks.length;
|
||||||
|
activeAnnotationId = sortedBlocks[next].annotationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMode() {
|
||||||
|
if (canWrite) panelMode = panelMode === 'read' ? 'edit' : 'read';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training enrollment is document-level — two fixed script-type chips
|
||||||
|
// (KURRENT_RECOGNITION / KURRENT_SEGMENTATION); there is no per-region training
|
||||||
|
// flag (that would arrive with #321). "t" toggles the primary recognition
|
||||||
|
// enrollment and stays a no-op unless a region is active, so it reads as an
|
||||||
|
// action on the region the transcriber is working on.
|
||||||
|
const RECOGNITION_TRAINING_LABEL = 'KURRENT_RECOGNITION';
|
||||||
|
|
||||||
|
function toggleTrainingMark() {
|
||||||
|
if (!activeAnnotationId) return;
|
||||||
|
const enrolled = !(doc.trainingLabels ?? []).includes(RECOGNITION_TRAINING_LABEL);
|
||||||
|
transcription.toggleTrainingLabel(RECOGNITION_TRAINING_LABEL, enrolled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCurrentRegion() {
|
||||||
|
if (activeAnnotationId) handleAnnotationDeleteRequest(activeAnnotationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disarm the draw cue whenever we leave edit mode.
|
||||||
|
$effect(() => {
|
||||||
|
if (panelMode !== 'edit') drawArmed = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shortcutOptions = {
|
||||||
|
isPanelOpen: () => transcribeMode,
|
||||||
|
isCheatsheetOpen: () => cheatsheetOpen,
|
||||||
|
panelMode: () => panelMode,
|
||||||
|
goToNextRegion: () => goToRegion(1),
|
||||||
|
goToPrevRegion: () => goToRegion(-1),
|
||||||
|
toggleMode,
|
||||||
|
closePanel: () => (transcribeMode = false),
|
||||||
|
startDrawMode: () => (drawArmed = true),
|
||||||
|
toggleTrainingMark,
|
||||||
|
deleteCurrentRegion,
|
||||||
|
openCheatsheet: () => (cheatsheetOpen = true)
|
||||||
|
};
|
||||||
|
|
||||||
function handleBlockFocus(blockId: string) {
|
function handleBlockFocus(blockId: string) {
|
||||||
const block = transcription.blocks.find((b) => b.id === blockId);
|
const block = transcription.blocks.find((b) => b.id === blockId);
|
||||||
if (block) {
|
if (block) {
|
||||||
@@ -208,14 +269,9 @@ onMount(() => {
|
|||||||
onStripUrl: () => replaceState(page.url.pathname, page.state ?? {})
|
onStripUrl: () => replaceState(page.url.pathname, page.state ?? {})
|
||||||
}).catch((e) => console.error('deep-link scroll failed', e));
|
}).catch((e) => console.error('deep-link scroll failed', e));
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
// Esc is owned solely by the transcribeShortcuts action (issue #327, decision
|
||||||
if (e.key === 'Escape' && transcribeMode) {
|
// B1) — no competing inline keydown listener here.
|
||||||
transcribeMode = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('keydown', onKeyDown);
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
|
||||||
ocrJob.destroy();
|
ocrJob.destroy();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -229,6 +285,7 @@ onMount(() => {
|
|||||||
class="fixed right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden bg-surface"
|
class="fixed right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden bg-surface"
|
||||||
style="top: {navHeight}px"
|
style="top: {navHeight}px"
|
||||||
data-hydrated
|
data-hydrated
|
||||||
|
use:transcribeShortcuts={shortcutOptions}
|
||||||
>
|
>
|
||||||
<DocumentTopBar
|
<DocumentTopBar
|
||||||
doc={doc}
|
doc={doc}
|
||||||
@@ -260,7 +317,7 @@ onMount(() => {
|
|||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
onAnnotationClick={handleAnnotationClick}
|
onAnnotationClick={handleAnnotationClick}
|
||||||
onTranscriptionDraw={createBlockFromDraw}
|
onTranscriptionDraw={createBlockFromDraw}
|
||||||
onDeleteAnnotationRequest={handleAnnotationDeleteRequest}
|
onAnnotationFocus={(id) => (activeAnnotationId = id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -369,4 +426,15 @@ onMount(() => {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if drawArmed && panelMode === 'edit'}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full bg-ink px-4 py-2 font-sans text-xs text-white shadow-lg"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
{m.shortcut_draw_hint()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ShortcutCheatsheet open={cheatsheetOpen} onClose={() => (cheatsheetOpen = false)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user