@semiont/react-ui 0.2.33-build.81 → 0.2.33-build.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{PdfAnnotationCanvas.client-RAJRPQLU.mjs → PdfAnnotationCanvas.client-FGV33CWN.mjs} +9 -14
- package/dist/PdfAnnotationCanvas.client-FGV33CWN.mjs.map +1 -0
- package/dist/chunk-FC6SGLLT.mjs +141 -0
- package/dist/chunk-FC6SGLLT.mjs.map +1 -0
- package/dist/chunk-XS27QKGP.mjs +55 -0
- package/dist/chunk-XS27QKGP.mjs.map +1 -0
- package/dist/{chunk-QB52Q7EQ.mjs → chunk-YPYLOBA2.mjs} +31 -81
- package/dist/chunk-YPYLOBA2.mjs.map +1 -0
- package/dist/index.css +16 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +70 -28
- package/dist/index.mjs +564 -621
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.mjs +5 -3
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/annotation/AnnotateToolbar.tsx +4 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -17
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -17
- package/src/components/resource/BrowseView.tsx +8 -8
- package/src/components/resource/__tests__/BrowseView.test.tsx +20 -12
- package/src/components/resource/panels/AssessmentEntry.tsx +3 -6
- package/src/components/resource/panels/CommentEntry.tsx +3 -6
- package/src/components/resource/panels/HighlightEntry.tsx +3 -6
- package/src/components/resource/panels/ReferenceEntry.tsx +3 -6
- package/src/components/resource/panels/TagEntry.tsx +3 -6
- package/src/components/resource/panels/TaggingPanel.tsx +5 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +42 -4
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +44 -0
- package/src/components/toolbar/Toolbar.css +20 -0
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +312 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +4 -8
- package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +266 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +5 -3
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +0 -1
- package/dist/chunk-QB52Q7EQ.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { ReferenceResolutionWidget } from '../lib/codemirror-widgets';
|
|
|
9
9
|
import { isHighlight, isReference, isResolvedReference, isComment, isAssessment, isTag, getBodySource } from '@semiont/api-client';
|
|
10
10
|
import type { components } from '@semiont/api-client';
|
|
11
11
|
import type { EventBus } from '../contexts/EventBusContext';
|
|
12
|
+
import { createHoverHandlers } from '../hooks/useAttentionFlow';
|
|
12
13
|
|
|
13
14
|
type Annotation = components['schemas']['Annotation'];
|
|
14
15
|
|
|
@@ -409,23 +410,21 @@ export function CodeMirrorRenderer({
|
|
|
409
410
|
// Attach hover event listeners using native DOM events with delegation
|
|
410
411
|
const container = view.dom;
|
|
411
412
|
|
|
413
|
+
const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
|
|
414
|
+
(annotationId) => eventBusRef.current?.emit('annotation:hover', { annotationId })
|
|
415
|
+
);
|
|
416
|
+
|
|
412
417
|
const handleMouseOver = (e: MouseEvent) => {
|
|
413
418
|
const target = e.target as HTMLElement;
|
|
414
419
|
const annotationElement = target.closest('[data-annotation-id]');
|
|
415
420
|
const annotationId = annotationElement?.getAttribute('data-annotation-id');
|
|
416
|
-
|
|
417
|
-
if (annotationId && eventBusRef.current) {
|
|
418
|
-
eventBusRef.current.emit('annotation:hover', { annotationId });
|
|
419
|
-
}
|
|
421
|
+
if (annotationId) handleMouseEnter(annotationId);
|
|
420
422
|
};
|
|
421
423
|
|
|
422
424
|
const handleMouseOut = (e: MouseEvent) => {
|
|
423
425
|
const target = e.target as HTMLElement;
|
|
424
426
|
const annotationElement = target.closest('[data-annotation-id]');
|
|
425
|
-
|
|
426
|
-
if (annotationElement && eventBusRef.current) {
|
|
427
|
-
eventBusRef.current.emit('annotation:hover', { annotationId: null });
|
|
428
|
-
}
|
|
427
|
+
if (annotationElement) handleMouseLeave();
|
|
429
428
|
};
|
|
430
429
|
|
|
431
430
|
container.addEventListener('mouseover', handleMouseOver);
|
|
@@ -434,6 +433,7 @@ export function CodeMirrorRenderer({
|
|
|
434
433
|
return () => {
|
|
435
434
|
container.removeEventListener('mouseover', handleMouseOver);
|
|
436
435
|
container.removeEventListener('mouseout', handleMouseOut);
|
|
436
|
+
cleanupHover();
|
|
437
437
|
view.destroy();
|
|
438
438
|
viewRef.current = null;
|
|
439
439
|
};
|
|
@@ -299,7 +299,10 @@ export function AnnotateToolbar({
|
|
|
299
299
|
onPin={() => setClickPinned(!clickPinned)}
|
|
300
300
|
containerRef={clickRef}
|
|
301
301
|
collapsedContent={
|
|
302
|
-
<div
|
|
302
|
+
<div
|
|
303
|
+
className="semiont-dropdown-display"
|
|
304
|
+
data-delete={clickActions.find(a => a.action === selectedClick)?.isDelete ? 'true' : 'false'}
|
|
305
|
+
>
|
|
303
306
|
<span className="semiont-dropdown-icon">
|
|
304
307
|
{clickActions.find(a => a.action === selectedClick)?.icon}
|
|
305
308
|
</span>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import type { components } from '@semiont/api-client';
|
|
5
|
+
import { createHoverHandlers } from '../../hooks/useAttentionFlow';
|
|
5
6
|
import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
|
|
6
7
|
import { parseSvgSelector } from '@semiont/api-client';
|
|
7
8
|
import type { EventBus } from '../../contexts/EventBusContext';
|
|
@@ -77,22 +78,10 @@ export function AnnotationOverlay({
|
|
|
77
78
|
const scaleX = displayWidth / imageWidth;
|
|
78
79
|
const scaleY = displayHeight / imageHeight;
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (currentHover.current !== annotationId) {
|
|
85
|
-
currentHover.current = annotationId;
|
|
86
|
-
eventBus?.emit('annotation:hover', { annotationId });
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const handleMouseLeave = () => {
|
|
91
|
-
if (currentHover.current !== null) {
|
|
92
|
-
currentHover.current = null;
|
|
93
|
-
eventBus?.emit('annotation:hover', { annotationId: null });
|
|
94
|
-
}
|
|
95
|
-
};
|
|
81
|
+
const { handleMouseEnter, handleMouseLeave } = useMemo(
|
|
82
|
+
() => createHoverHandlers((annotationId) => eventBus?.emit('annotation:hover', { annotationId })),
|
|
83
|
+
[eventBus]
|
|
84
|
+
);
|
|
96
85
|
|
|
97
86
|
return (
|
|
98
87
|
<svg
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
|
|
4
|
+
import { createHoverHandlers } from '../../hooks/useAttentionFlow';
|
|
4
5
|
import type { components, ResourceUri } from '@semiont/api-client';
|
|
5
6
|
import { getTargetSelector } from '@semiont/api-client';
|
|
6
7
|
import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
|
|
@@ -97,9 +98,6 @@ export function PdfAnnotationCanvas({
|
|
|
97
98
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
98
99
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
99
100
|
|
|
100
|
-
// Track current hover state to prevent redundant emissions
|
|
101
|
-
const currentHover = useRef<string | null>(null);
|
|
102
|
-
|
|
103
101
|
// Load PDF document on mount
|
|
104
102
|
useEffect(() => {
|
|
105
103
|
let cancelled = false;
|
|
@@ -362,20 +360,11 @@ export function PdfAnnotationCanvas({
|
|
|
362
360
|
return page === pageNumber;
|
|
363
361
|
});
|
|
364
362
|
|
|
365
|
-
// Hover handlers with
|
|
366
|
-
const handleMouseEnter = (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const handleMouseLeave = () => {
|
|
374
|
-
if (currentHover.current !== null) {
|
|
375
|
-
currentHover.current = null;
|
|
376
|
-
eventBus?.emit('annotation:hover', { annotationId: null });
|
|
377
|
-
}
|
|
378
|
-
};
|
|
363
|
+
// Hover handlers with currentHover guard and dwell delay
|
|
364
|
+
const { handleMouseEnter, handleMouseLeave } = useMemo(
|
|
365
|
+
() => createHoverHandlers((annotationId) => eventBus?.emit('annotation:hover', { annotationId })),
|
|
366
|
+
[eventBus]
|
|
367
|
+
);
|
|
379
368
|
|
|
380
369
|
// Calculate motivation color
|
|
381
370
|
const { stroke, fill } = getMotivationColor(selectedMotivation ?? null);
|
|
@@ -8,6 +8,7 @@ import { rehypeRenderAnnotations } from '../../lib/rehype-render-annotations';
|
|
|
8
8
|
import type { components } from '@semiont/api-client';
|
|
9
9
|
import { getExactText, getTextPositionSelector, getTargetSelector, getBodySource, getMimeCategory, isPdfMimeType, resourceUri as toResourceUri } from '@semiont/api-client';
|
|
10
10
|
import { ANNOTATORS } from '../../lib/annotation-registry';
|
|
11
|
+
import { createHoverHandlers } from '../../hooks/useAttentionFlow';
|
|
11
12
|
import { ImageViewer } from '../viewers';
|
|
12
13
|
import { AnnotateToolbar, type ClickAction } from '../annotation/AnnotateToolbar';
|
|
13
14
|
import type { AnnotationsCollection } from '../../types/annotation-props';
|
|
@@ -115,25 +116,23 @@ export function BrowseView({
|
|
|
115
116
|
}
|
|
116
117
|
};
|
|
117
118
|
|
|
119
|
+
const { handleMouseEnter, handleMouseLeave, cleanup: cleanupHover } = createHoverHandlers(
|
|
120
|
+
(annotationId) => eventBus.emit('annotation:hover', { annotationId })
|
|
121
|
+
);
|
|
122
|
+
|
|
118
123
|
// Single mouseover handler for the container - fires once on enter
|
|
119
124
|
const handleMouseOver = (e: MouseEvent) => {
|
|
120
125
|
const target = e.target as HTMLElement;
|
|
121
126
|
const annotationElement = target.closest('[data-annotation-id]');
|
|
122
127
|
const annotationId = annotationElement?.getAttribute('data-annotation-id');
|
|
123
|
-
|
|
124
|
-
if (annotationId) {
|
|
125
|
-
eventBus.emit('annotation:hover', { annotationId });
|
|
126
|
-
}
|
|
128
|
+
if (annotationId) handleMouseEnter(annotationId);
|
|
127
129
|
};
|
|
128
130
|
|
|
129
131
|
// Single mouseout handler for the container - fires once on exit
|
|
130
132
|
const handleMouseOut = (e: MouseEvent) => {
|
|
131
133
|
const target = e.target as HTMLElement;
|
|
132
134
|
const annotationElement = target.closest('[data-annotation-id]');
|
|
133
|
-
|
|
134
|
-
if (annotationElement) {
|
|
135
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
136
|
-
}
|
|
135
|
+
if (annotationElement) handleMouseLeave();
|
|
137
136
|
};
|
|
138
137
|
|
|
139
138
|
// Apply animation classes to new annotations
|
|
@@ -155,6 +154,7 @@ export function BrowseView({
|
|
|
155
154
|
container.removeEventListener('click', handleClick);
|
|
156
155
|
container.removeEventListener('mouseover', handleMouseOver);
|
|
157
156
|
container.removeEventListener('mouseout', handleMouseOut);
|
|
157
|
+
cleanupHover();
|
|
158
158
|
};
|
|
159
159
|
}, [content, allAnnotations, newAnnotationIds]);
|
|
160
160
|
|
|
@@ -320,30 +320,38 @@ describe('BrowseView Component', () => {
|
|
|
320
320
|
});
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
-
it('should emit annotation:hover with null when mouse exits
|
|
323
|
+
it('should emit annotation:hover with null when mouse exits after dwell', async () => {
|
|
324
|
+
vi.useFakeTimers();
|
|
324
325
|
const tracker = createEventTracker();
|
|
325
|
-
const
|
|
326
|
+
const annotations = {
|
|
327
|
+
...defaultProps.annotations,
|
|
328
|
+
references: [createMockAnnotation('linking', 'ref-1')],
|
|
329
|
+
};
|
|
330
|
+
const { container } = renderWithEventTracking(
|
|
331
|
+
<BrowseView {...defaultProps} annotations={annotations} />,
|
|
332
|
+
tracker
|
|
333
|
+
);
|
|
326
334
|
|
|
327
335
|
const browseContainer = container.querySelector('.semiont-browse-view__content');
|
|
328
336
|
|
|
329
|
-
// Create annotation element
|
|
330
337
|
const mockAnnotationElement = document.createElement('span');
|
|
331
338
|
mockAnnotationElement.setAttribute('data-annotation-id', 'ref-1');
|
|
339
|
+
const mockTarget = { closest: vi.fn(() => mockAnnotationElement) } as any;
|
|
332
340
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
341
|
+
// Enter and let dwell timer fire
|
|
342
|
+
fireEvent.mouseOver(browseContainer!, { target: mockTarget });
|
|
343
|
+
vi.advanceTimersByTime(200);
|
|
336
344
|
|
|
337
345
|
tracker.clear();
|
|
338
346
|
|
|
339
|
-
//
|
|
347
|
+
// Now exit — should emit null
|
|
340
348
|
fireEvent.mouseOut(browseContainer!, { target: mockTarget });
|
|
341
349
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
expect(tracker.events.some(e =>
|
|
351
|
+
e.event === 'annotation:hover' && e.payload?.annotationId === null
|
|
352
|
+
)).toBe(true);
|
|
353
|
+
|
|
354
|
+
vi.useRealTimers();
|
|
347
355
|
});
|
|
348
356
|
|
|
349
357
|
it('should not emit on mouseover when not over annotation', async () => {
|
|
@@ -4,6 +4,7 @@ import { forwardRef } from 'react';
|
|
|
4
4
|
import type { components } from '@semiont/api-client';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/api-client';
|
|
6
6
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
7
|
+
import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
|
|
7
8
|
|
|
8
9
|
type Annotation = components['schemas']['Annotation'];
|
|
9
10
|
|
|
@@ -76,6 +77,7 @@ export const AssessmentEntry = forwardRef<HTMLDivElement, AssessmentEntryProps>(
|
|
|
76
77
|
ref
|
|
77
78
|
) {
|
|
78
79
|
const eventBus = useEventBus();
|
|
80
|
+
const hoverProps = useHoverEmitter(assessment.id);
|
|
79
81
|
|
|
80
82
|
const selectedText = getAnnotationExactText(assessment);
|
|
81
83
|
const assessmentText = getAssessmentText(assessment);
|
|
@@ -89,12 +91,7 @@ export const AssessmentEntry = forwardRef<HTMLDivElement, AssessmentEntryProps>(
|
|
|
89
91
|
onClick={() => {
|
|
90
92
|
eventBus.emit('annotation:click', { annotationId: assessment.id, motivation: assessment.motivation });
|
|
91
93
|
}}
|
|
92
|
-
|
|
93
|
-
eventBus.emit('annotation:hover', { annotationId: assessment.id });
|
|
94
|
-
}}
|
|
95
|
-
onMouseLeave={() => {
|
|
96
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
97
|
-
}}
|
|
94
|
+
{...hoverProps}
|
|
98
95
|
>
|
|
99
96
|
{/* Selected text quote */}
|
|
100
97
|
{selectedText && (
|
|
@@ -5,6 +5,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
|
|
|
5
5
|
import type { components } from '@semiont/api-client';
|
|
6
6
|
import { getAnnotationExactText, getCommentText } from '@semiont/api-client';
|
|
7
7
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
8
|
+
import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
|
|
8
9
|
|
|
9
10
|
type Annotation = components['schemas']['Annotation'];
|
|
10
11
|
|
|
@@ -44,6 +45,7 @@ export const CommentEntry = forwardRef<HTMLDivElement, CommentEntryProps>(
|
|
|
44
45
|
) {
|
|
45
46
|
const t = useTranslations('CommentsPanel');
|
|
46
47
|
const eventBus = useEventBus();
|
|
48
|
+
const hoverProps = useHoverEmitter(comment.id);
|
|
47
49
|
const [isEditing, setIsEditing] = useState(false);
|
|
48
50
|
const [editText, setEditText] = useState('');
|
|
49
51
|
const internalRef = useRef<HTMLDivElement>(null);
|
|
@@ -88,12 +90,7 @@ export const CommentEntry = forwardRef<HTMLDivElement, CommentEntryProps>(
|
|
|
88
90
|
onClick={() => {
|
|
89
91
|
eventBus.emit('annotation:click', { annotationId: comment.id, motivation: comment.motivation });
|
|
90
92
|
}}
|
|
91
|
-
|
|
92
|
-
eventBus.emit('annotation:hover', { annotationId: comment.id });
|
|
93
|
-
}}
|
|
94
|
-
onMouseLeave={() => {
|
|
95
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
96
|
-
}}
|
|
93
|
+
{...hoverProps}
|
|
97
94
|
>
|
|
98
95
|
{/* Selected text quote - only for text annotations */}
|
|
99
96
|
{selectedText && (
|
|
@@ -4,6 +4,7 @@ import { forwardRef } from 'react';
|
|
|
4
4
|
import type { components } from '@semiont/api-client';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/api-client';
|
|
6
6
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
7
|
+
import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
|
|
7
8
|
|
|
8
9
|
type Annotation = components['schemas']['Annotation'];
|
|
9
10
|
|
|
@@ -40,6 +41,7 @@ export const HighlightEntry = forwardRef<HTMLDivElement, HighlightEntryProps>(
|
|
|
40
41
|
ref
|
|
41
42
|
) {
|
|
42
43
|
const eventBus = useEventBus();
|
|
44
|
+
const hoverProps = useHoverEmitter(highlight.id);
|
|
43
45
|
|
|
44
46
|
const selectedText = getAnnotationExactText(highlight);
|
|
45
47
|
|
|
@@ -52,12 +54,7 @@ export const HighlightEntry = forwardRef<HTMLDivElement, HighlightEntryProps>(
|
|
|
52
54
|
onClick={() => {
|
|
53
55
|
eventBus.emit('annotation:click', { annotationId: highlight.id, motivation: highlight.motivation });
|
|
54
56
|
}}
|
|
55
|
-
|
|
56
|
-
eventBus.emit('annotation:hover', { annotationId: highlight.id });
|
|
57
|
-
}}
|
|
58
|
-
onMouseLeave={() => {
|
|
59
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
60
|
-
}}
|
|
57
|
+
{...hoverProps}
|
|
61
58
|
>
|
|
62
59
|
{/* Highlighted text */}
|
|
63
60
|
{selectedText && (
|
|
@@ -9,6 +9,7 @@ import { getEntityTypes } from '@semiont/ontology';
|
|
|
9
9
|
import { getResourceIcon } from '../../../lib/resource-utils';
|
|
10
10
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
11
11
|
import { useObservableExternalNavigation } from '../../../hooks/useObservableNavigation';
|
|
12
|
+
import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
|
|
12
13
|
|
|
13
14
|
type Annotation = components['schemas']['Annotation'];
|
|
14
15
|
|
|
@@ -42,6 +43,7 @@ export const ReferenceEntry = forwardRef<HTMLDivElement, ReferenceEntryProps>(
|
|
|
42
43
|
const t = useTranslations('ReferencesPanel');
|
|
43
44
|
const eventBus = useEventBus();
|
|
44
45
|
const navigate = useObservableExternalNavigation();
|
|
46
|
+
const hoverProps = useHoverEmitter(reference.id);
|
|
45
47
|
|
|
46
48
|
const selectedText = getAnnotationExactText(reference) || '';
|
|
47
49
|
const isResolved = isBodyResolved(reference.body);
|
|
@@ -121,12 +123,7 @@ export const ReferenceEntry = forwardRef<HTMLDivElement, ReferenceEntryProps>(
|
|
|
121
123
|
onClick={() => {
|
|
122
124
|
eventBus.emit('annotation:click', { annotationId: reference.id, motivation: reference.motivation });
|
|
123
125
|
}}
|
|
124
|
-
|
|
125
|
-
eventBus.emit('annotation:hover', { annotationId: reference.id });
|
|
126
|
-
}}
|
|
127
|
-
onMouseLeave={() => {
|
|
128
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
129
|
-
}}
|
|
126
|
+
{...hoverProps}
|
|
130
127
|
>
|
|
131
128
|
{/* Status indicator and text quote */}
|
|
132
129
|
<div className="semiont-annotation-entry__header">
|
|
@@ -6,6 +6,7 @@ import { getAnnotationExactText } from '@semiont/api-client';
|
|
|
6
6
|
import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
|
|
7
7
|
import { getTagSchema } from '../../../lib/tag-schemas';
|
|
8
8
|
import { useEventBus } from '../../../contexts/EventBusContext';
|
|
9
|
+
import { useHoverEmitter } from '../../../hooks/useAttentionFlow';
|
|
9
10
|
|
|
10
11
|
type Annotation = components['schemas']['Annotation'];
|
|
11
12
|
|
|
@@ -25,6 +26,7 @@ export const TagEntry = forwardRef<HTMLDivElement, TagEntryProps>(
|
|
|
25
26
|
ref
|
|
26
27
|
) {
|
|
27
28
|
const eventBus = useEventBus();
|
|
29
|
+
const hoverProps = useHoverEmitter(tag.id);
|
|
28
30
|
|
|
29
31
|
const selectedText = getAnnotationExactText(tag);
|
|
30
32
|
const category = getTagCategory(tag);
|
|
@@ -37,12 +39,7 @@ export const TagEntry = forwardRef<HTMLDivElement, TagEntryProps>(
|
|
|
37
39
|
onClick={() => {
|
|
38
40
|
eventBus.emit('annotation:click', { annotationId: tag.id, motivation: tag.motivation });
|
|
39
41
|
}}
|
|
40
|
-
|
|
41
|
-
eventBus.emit('annotation:hover', { annotationId: tag.id });
|
|
42
|
-
}}
|
|
43
|
-
onMouseLeave={() => {
|
|
44
|
-
eventBus.emit('annotation:hover', { annotationId: null });
|
|
45
|
-
}}
|
|
42
|
+
{...hoverProps}
|
|
46
43
|
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
47
44
|
data-type="tag"
|
|
48
45
|
data-focused={isFocused ? 'true' : 'false'}
|
|
@@ -273,7 +273,8 @@ describe('CommentEntry Component', () => {
|
|
|
273
273
|
});
|
|
274
274
|
|
|
275
275
|
describe('Hover Interactions', () => {
|
|
276
|
-
it('should emit annotation:hover event with annotation id
|
|
276
|
+
it('should emit annotation:hover event with annotation id after dwell delay', () => {
|
|
277
|
+
vi.useFakeTimers();
|
|
277
278
|
const hoverHandler = vi.fn();
|
|
278
279
|
|
|
279
280
|
const { container, eventBus } = renderWithProviders(
|
|
@@ -287,13 +288,21 @@ describe('CommentEntry Component', () => {
|
|
|
287
288
|
const commentDiv = container.firstChild as HTMLElement;
|
|
288
289
|
fireEvent.mouseEnter(commentDiv);
|
|
289
290
|
|
|
291
|
+
// Not emitted immediately — dwell timer pending
|
|
292
|
+
expect(hoverHandler).not.toHaveBeenCalled();
|
|
293
|
+
|
|
294
|
+
// Advance past HOVER_DELAY_MS
|
|
295
|
+
vi.advanceTimersByTime(200);
|
|
296
|
+
|
|
290
297
|
expect(hoverHandler).toHaveBeenCalledWith({ annotationId: 'comment-1' });
|
|
291
298
|
|
|
292
299
|
// Clean up
|
|
293
300
|
eventBus!.off('annotation:hover', hoverHandler);
|
|
301
|
+
vi.useRealTimers();
|
|
294
302
|
});
|
|
295
303
|
|
|
296
|
-
it('should emit annotation:hover
|
|
304
|
+
it('should NOT emit annotation:hover when mouse leaves before dwell delay', () => {
|
|
305
|
+
vi.useFakeTimers();
|
|
297
306
|
const hoverHandler = vi.fn();
|
|
298
307
|
|
|
299
308
|
const { container, eventBus } = renderWithProviders(
|
|
@@ -305,12 +314,41 @@ describe('CommentEntry Component', () => {
|
|
|
305
314
|
eventBus!.on('annotation:hover', hoverHandler);
|
|
306
315
|
|
|
307
316
|
const commentDiv = container.firstChild as HTMLElement;
|
|
308
|
-
fireEvent.
|
|
317
|
+
fireEvent.mouseEnter(commentDiv);
|
|
318
|
+
fireEvent.mouseLeave(commentDiv); // leaves before timer fires
|
|
319
|
+
|
|
320
|
+
vi.advanceTimersByTime(200);
|
|
309
321
|
|
|
310
|
-
|
|
322
|
+
// Timer was cancelled — no event emitted at all
|
|
323
|
+
expect(hoverHandler).not.toHaveBeenCalled();
|
|
311
324
|
|
|
312
325
|
// Clean up
|
|
313
326
|
eventBus!.off('annotation:hover', hoverHandler);
|
|
327
|
+
vi.useRealTimers();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should emit annotation:hover null after dwell then leave', () => {
|
|
331
|
+
vi.useFakeTimers();
|
|
332
|
+
const hoverHandler = vi.fn();
|
|
333
|
+
|
|
334
|
+
const { container, eventBus } = renderWithProviders(
|
|
335
|
+
<CommentEntry {...defaultProps} />,
|
|
336
|
+
{ returnEventBus: true }
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
eventBus!.on('annotation:hover', hoverHandler);
|
|
340
|
+
|
|
341
|
+
const commentDiv = container.firstChild as HTMLElement;
|
|
342
|
+
fireEvent.mouseEnter(commentDiv);
|
|
343
|
+
vi.advanceTimersByTime(200); // hover committed
|
|
344
|
+
|
|
345
|
+
fireEvent.mouseLeave(commentDiv);
|
|
346
|
+
|
|
347
|
+
expect(hoverHandler).toHaveBeenNthCalledWith(1, { annotationId: 'comment-1' });
|
|
348
|
+
expect(hoverHandler).toHaveBeenNthCalledWith(2, { annotationId: null });
|
|
349
|
+
|
|
350
|
+
eventBus!.off('annotation:hover', hoverHandler);
|
|
351
|
+
vi.useRealTimers();
|
|
314
352
|
});
|
|
315
353
|
|
|
316
354
|
it('should handle hover events without errors', () => {
|
|
@@ -386,6 +386,50 @@ describe('TaggingPanel Component', () => {
|
|
|
386
386
|
});
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
+
it('should include schema id as classifying body alongside tagging body', async () => {
|
|
390
|
+
// Regression: manual tags were missing the classifying body, so getTagSchemaId()
|
|
391
|
+
// returned undefined and TagEntry never showed the schema name.
|
|
392
|
+
const tracker = createEventTracker();
|
|
393
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
394
|
+
|
|
395
|
+
renderWithEventBus(
|
|
396
|
+
<TaggingPanel
|
|
397
|
+
{...defaultProps}
|
|
398
|
+
pendingAnnotation={pendingAnnotation}
|
|
399
|
+
/>,
|
|
400
|
+
tracker
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const categorySelects = screen.getAllByRole('combobox');
|
|
404
|
+
const categorySelect = categorySelects.find(select =>
|
|
405
|
+
select.querySelector('option[value=""]')?.textContent === 'Choose a category'
|
|
406
|
+
);
|
|
407
|
+
await userEvent.selectOptions(categorySelect!, 'Rule');
|
|
408
|
+
|
|
409
|
+
await waitFor(() => {
|
|
410
|
+
const createEvent = tracker.events.find(e => e.event === 'annotation:create');
|
|
411
|
+
expect(createEvent).toBeDefined();
|
|
412
|
+
const body: any[] = createEvent!.payload.body;
|
|
413
|
+
|
|
414
|
+
// Must have exactly two body elements
|
|
415
|
+
expect(body).toHaveLength(2);
|
|
416
|
+
|
|
417
|
+
// First: the category
|
|
418
|
+
expect(body[0]).toMatchObject({
|
|
419
|
+
type: 'TextualBody',
|
|
420
|
+
value: 'Rule',
|
|
421
|
+
purpose: 'tagging',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Second: the schema id — this is what was missing before the fix
|
|
425
|
+
expect(body[1]).toMatchObject({
|
|
426
|
+
type: 'TextualBody',
|
|
427
|
+
value: 'legal-irac',
|
|
428
|
+
purpose: 'classifying',
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
389
433
|
it('should have proper styling for tag creation form', () => {
|
|
390
434
|
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
391
435
|
|
|
@@ -79,6 +79,26 @@
|
|
|
79
79
|
color: var(--semiont-color-gray-300);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
.semiont-dropdown-display[data-delete="true"] {
|
|
83
|
+
background: var(--semiont-color-error);
|
|
84
|
+
color: var(--semiont-color-white);
|
|
85
|
+
border-radius: var(--semiont-radius-sm);
|
|
86
|
+
padding: 0.125rem 0.375rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.semiont-dropdown-display[data-delete="true"] .semiont-dropdown-label {
|
|
90
|
+
color: var(--semiont-color-white);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[data-theme="dark"] .semiont-dropdown-display[data-delete="true"] {
|
|
94
|
+
background: var(--semiont-color-error-dark);
|
|
95
|
+
color: var(--semiont-color-error-light);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[data-theme="dark"] .semiont-dropdown-display[data-delete="true"] .semiont-dropdown-label {
|
|
99
|
+
color: var(--semiont-color-error-light);
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
.semiont-dropdown-menu {
|
|
83
103
|
position: absolute;
|
|
84
104
|
top: 100%;
|