Skip to content

Commit 6160648

Browse files
author
Adrian Tompkins
committed
Improve translation panel interactions
1 parent 049b463 commit 6160648

2 files changed

Lines changed: 57 additions & 5 deletions

File tree

src/lib/components/Reader/TextBoxes.svelte

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@
620620
}
621621
622622
let openAiBlock = $state<number | null>(null);
623+
let openOcrBlock = $state<number | null>(null);
623624
let openAiWord = $state<number | null>(null);
624625
let mobileAiPanel = $state(false);
625626
@@ -640,12 +641,25 @@
640641
event.stopPropagation();
641642
if (window.getSelection()?.toString()) return;
642643
mobileAiPanel = window.matchMedia?.('(hover: none), (pointer: coarse)').matches ?? false;
644+
645+
// Touch has no persistent hover state. Use the first tap to reveal the OCR
646+
// and only open the AI panel when the same block is tapped again.
647+
if (mobileAiPanel && openOcrBlock !== blockIndex) {
648+
openOcrBlock = blockIndex;
649+
openAiBlock = null;
650+
openAiWord = null;
651+
return;
652+
}
653+
643654
openAiBlock = blockIndex;
644655
openAiWord = null;
645656
}
646657
647-
function closeAi() {
658+
function closeAi(event: PointerEvent) {
659+
const target = event.target as Element | null;
660+
if (target?.closest('.textBox, .aiPanel')) return;
648661
openAiBlock = null;
662+
openOcrBlock = null;
649663
openAiWord = null;
650664
}
651665
</script>
@@ -662,6 +676,7 @@
662676
class:perLine={usePerLine}
663677
class:forceVisible
664678
class:alwaysVisible={alwaysShowOCR}
679+
class:tapVisible={openOcrBlock === blockIndex}
665680
class:hasAi={Boolean(ai)}
666681
style:width={usePerLine ? width : isOriginalMode || useMinDimensions ? undefined : width}
667682
style:height={usePerLine ? height : isOriginalMode || useMinDimensions ? undefined : height}
@@ -800,12 +815,14 @@
800815
801816
/* Force visibility for placeholder/missing pages, or when always-show OCR is enabled */
802817
.textBox.forceVisible,
803-
.textBox.alwaysVisible {
818+
.textBox.alwaysVisible,
819+
.textBox.tapVisible {
804820
background: rgb(255, 255, 255);
805821
}
806822
807823
.textBox.forceVisible p,
808-
.textBox.alwaysVisible p {
824+
.textBox.alwaysVisible p,
825+
.textBox.tapVisible p {
809826
visibility: visible;
810827
}
811828
@@ -871,7 +888,7 @@
871888
position: absolute;
872889
left: 0;
873890
top: calc(100% + 30px);
874-
width: min(28rem, 80vw);
891+
width: min(40rem, 80vw);
875892
max-height: 60vh;
876893
overflow: auto;
877894
padding: 12px;

src/lib/components/Reader/__tests__/TextBoxes.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ describe('TextBoxes auto mode with lines_coords', () => {
104104
}
105105
});
106106

107-
await fireEvent.click(container.querySelector('.textBox.hasAi')!);
107+
const textBox = container.querySelector('.textBox.hasAi')!;
108+
await fireEvent.click(textBox);
109+
expect(textBox.classList.contains('tapVisible')).toBe(true);
110+
expect(document.body.querySelector('.aiPanel.mobileViewportPanel')).toBeNull();
111+
await fireEvent.click(textBox);
108112
const panel = document.body.querySelector('.aiPanel.mobileViewportPanel');
109113
expect(panel).toBeTruthy();
110114
expect(container.contains(panel)).toBe(false);
@@ -115,6 +119,37 @@ describe('TextBoxes auto mode with lines_coords', () => {
115119
}
116120
});
117121

122+
it('keeps one-click AI opening on desktop pointers', async () => {
123+
const originalMatchMedia = window.matchMedia;
124+
window.matchMedia = vi.fn().mockReturnValue({ matches: false });
125+
126+
try {
127+
const { container, getByText } = render(TextBoxes, {
128+
page: makePage([blockWithCoords]),
129+
volumeUuid: 'test-uuid',
130+
aiPage: {
131+
page_index: 0,
132+
img_path: 'page_001.jpg',
133+
blocks: [
134+
{
135+
block_index: 0,
136+
block_key: 'key',
137+
source_lines: blockWithCoords.lines,
138+
corrected_lines: blockWithCoords.lines,
139+
translation: 'Desktop translation.',
140+
words: []
141+
}
142+
]
143+
}
144+
});
145+
146+
await fireEvent.click(container.querySelector('.textBox.hasAi')!);
147+
expect(getByText('Desktop translation.')).toBeTruthy();
148+
} finally {
149+
window.matchMedia = originalMatchMedia;
150+
}
151+
});
152+
118153
it('opens translation and contextual word details from an AI sidecar page', async () => {
119154
const { container, getByText, queryByText } = render(TextBoxes, {
120155
page: makePage([blockWithCoords]),

0 commit comments

Comments
 (0)