@semiont/react-ui 0.4.20 → 0.4.22

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.
Files changed (125) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
  3. package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-4NOUO3W6.mjs +7788 -0
  6. package/dist/chunk-4NOUO3W6.mjs.map +1 -0
  7. package/dist/index.d.mts +212 -1206
  8. package/dist/index.mjs +3332 -13712
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +48 -21
  11. package/dist/test-utils.mjs +2505 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +12 -12
  16. package/src/components/LiveRegion.tsx +1 -2
  17. package/src/components/StatusDisplay.tsx +42 -16
  18. package/src/components/Toolbar.tsx +4 -4
  19. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  20. package/src/components/__tests__/StatusDisplay.test.tsx +50 -65
  21. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  22. package/src/components/annotation/AnnotateToolbar.tsx +8 -9
  23. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  24. package/src/components/annotation-popups/JsonLdView.tsx +1 -2
  25. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
  26. package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
  27. package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
  28. package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
  29. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  30. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  31. package/src/components/modals/ResourceSearchModal.tsx +12 -8
  32. package/src/components/modals/SearchModal.tsx +11 -6
  33. package/src/components/modals/SearchResultsStep.tsx +1 -3
  34. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  35. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  36. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  37. package/src/components/modals/__tests__/SearchModal.accessibility.test.tsx +6 -2
  38. package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
  39. package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
  40. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  41. package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
  42. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  43. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  44. package/src/components/navigation/ObservableLink.tsx +6 -6
  45. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  46. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  47. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  48. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +15 -18
  49. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
  50. package/src/components/resource/AnnotateView.tsx +8 -10
  51. package/src/components/resource/AnnotationHistory.tsx +9 -12
  52. package/src/components/resource/BrowseView.tsx +11 -8
  53. package/src/components/resource/ResourceViewer.tsx +22 -34
  54. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  55. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
  56. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
  57. package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
  58. package/src/components/resource/event-formatting.ts +2 -3
  59. package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
  60. package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
  61. package/src/components/resource/panels/AssistSection.tsx +15 -21
  62. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  63. package/src/components/resource/panels/CommentEntry.tsx +7 -8
  64. package/src/components/resource/panels/CommentsPanel.tsx +11 -13
  65. package/src/components/resource/panels/HighlightEntry.tsx +7 -8
  66. package/src/components/resource/panels/HighlightPanel.tsx +12 -13
  67. package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
  68. package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
  69. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
  70. package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
  71. package/src/components/resource/panels/TagEntry.tsx +7 -8
  72. package/src/components/resource/panels/TaggingPanel.tsx +14 -23
  73. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
  74. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
  75. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
  76. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  77. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
  78. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
  79. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
  80. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
  81. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
  82. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  83. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  84. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +28 -53
  85. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
  88. package/src/components/settings/SettingsPanel.tsx +9 -9
  89. package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
  90. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
  91. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  92. package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
  93. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
  94. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  95. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
  96. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
  97. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  98. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  99. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  100. package/src/features/resource-compose/components/ResourceComposePage.tsx +6 -22
  101. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  102. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
  103. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
  104. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
  105. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
  106. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  107. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  108. package/dist/chunk-OZICDVH7.mjs +0 -62
  109. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  110. package/dist/chunk-R4CCMFJH.mjs +0 -877
  111. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  112. package/dist/chunk-VN5NY4SN.mjs +0 -200
  113. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  114. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  115. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  116. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  117. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  118. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  119. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  120. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  121. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  122. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  123. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  124. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  125. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -4,7 +4,8 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import '@testing-library/jest-dom';
6
6
  import { ReferencesPanel } from '../ReferencesPanel';
7
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
7
+ import type { EventBus } from '@semiont/core';
8
+ import { createTestSemiontWrapper } from '../../../../test-utils';
8
9
 
9
10
  // Composition-based event tracker
10
11
  interface TrackedEvent {
@@ -14,59 +15,27 @@ interface TrackedEvent {
14
15
 
15
16
  function createEventTracker() {
16
17
  const events: TrackedEvent[] = [];
17
-
18
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
19
- const eventBus = useEventBus();
20
-
21
- React.useEffect(() => {
22
- const handlers: Array<() => void> = [];
23
-
24
- const trackEvent = (eventName: string) => (payload: any) => {
25
- events.push({ event: eventName, payload });
26
- };
27
-
28
- const panelEvents = ['mark:assist-request'] as const;
29
-
30
- panelEvents.forEach(eventName => {
31
- const handler = trackEvent(eventName);
32
- const subscription = eventBus.get(eventName).subscribe(handler);
33
- handlers.push(subscription);
34
- });
35
-
36
- return () => {
37
- handlers.forEach(sub => sub.unsubscribe());
38
- };
39
- }, [eventBus]);
40
-
41
- return <>{children}</>;
42
- }
43
-
44
18
  return {
45
- EventTrackingWrapper,
46
19
  events,
47
- clear: () => {
48
- events.length = 0;
20
+ clear: () => { events.length = 0; },
21
+ _attach(eventBus: EventBus) {
22
+ const panelEvents = ['mark:assist-request'] as const;
23
+ panelEvents.forEach((eventName) => {
24
+ eventBus.get(eventName).subscribe((payload: any) => {
25
+ events.push({ event: eventName, payload });
26
+ });
27
+ });
49
28
  },
50
29
  };
51
30
  }
52
31
 
53
- // Helper to render with EventBusProvider
54
32
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
55
- if (tracker) {
56
- return render(
57
- <EventBusProvider>
58
- <tracker.EventTrackingWrapper>
59
- {component}
60
- </tracker.EventTrackingWrapper>
61
- </EventBusProvider>
62
- );
63
- }
64
-
65
- return render(
66
- <EventBusProvider>
67
- {component}
68
- </EventBusProvider>
33
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
34
+ if (tracker) tracker._attach(eventBus);
35
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
36
+ <SemiontWrapper>{children}</SemiontWrapper>
69
37
  );
38
+ return render(component, { wrapper: Wrapper });
70
39
  };
71
40
 
72
41
  // Mock TranslationContext
@@ -344,26 +313,22 @@ describe('ReferencesPanel Component', () => {
344
313
 
345
314
  // Simulate detection starting
346
315
  rerender(
347
- <EventBusProvider>
348
- <ReferencesPanel
349
- {...defaultProps}
350
- isAssisting={true}
351
- progress={{ completedEntityTypes: [] }}
352
- />
353
- </EventBusProvider>
316
+ <ReferencesPanel
317
+ {...defaultProps}
318
+ isAssisting={true}
319
+ progress={{ completedEntityTypes: [] }}
320
+ />
354
321
  );
355
322
 
356
323
  // Simulate detection completing
357
324
  rerender(
358
- <EventBusProvider>
359
- <ReferencesPanel
360
- {...defaultProps}
361
- isAssisting={false}
362
- progress={{
363
- completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
364
- }}
365
- />
366
- </EventBusProvider>
325
+ <ReferencesPanel
326
+ {...defaultProps}
327
+ isAssisting={false}
328
+ progress={{
329
+ completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
330
+ }}
331
+ />
367
332
  );
368
333
 
369
334
  // UI should reset but we can't directly test internal state
@@ -473,13 +438,11 @@ describe('ReferencesPanel Component', () => {
473
438
 
474
439
  // Parent clears progress after completion
475
440
  rerender(
476
- <EventBusProvider>
477
- <ReferencesPanel
478
- {...defaultProps}
479
- isAssisting={false}
480
- progress={null}
481
- />
482
- </EventBusProvider>
441
+ <ReferencesPanel
442
+ {...defaultProps}
443
+ isAssisting={false}
444
+ progress={null}
445
+ />
483
446
  );
484
447
 
485
448
  expect(screen.getByText('Person:')).toBeInTheDocument();
@@ -498,9 +461,7 @@ describe('ReferencesPanel Component', () => {
498
461
  );
499
462
 
500
463
  rerender(
501
- <EventBusProvider>
502
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
503
- </EventBusProvider>
464
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
504
465
  );
505
466
  expect(screen.getByText(/Found.*5/i)).toBeInTheDocument();
506
467
  });
@@ -517,9 +478,7 @@ describe('ReferencesPanel Component', () => {
517
478
  );
518
479
 
519
480
  rerender(
520
- <EventBusProvider>
521
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
522
- </EventBusProvider>
481
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
523
482
  );
524
483
  expect(screen.getByText('✓')).toBeInTheDocument();
525
484
  });
@@ -536,9 +495,7 @@ describe('ReferencesPanel Component', () => {
536
495
  );
537
496
 
538
497
  rerender(
539
- <EventBusProvider>
540
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
541
- </EventBusProvider>
498
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
542
499
  );
543
500
 
544
501
  // Should show both the completed log AND the selection UI
@@ -558,9 +515,7 @@ describe('ReferencesPanel Component', () => {
558
515
  );
559
516
 
560
517
  rerender(
561
- <EventBusProvider>
562
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
563
- </EventBusProvider>
518
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
564
519
  );
565
520
 
566
521
  // Selection UI should be immediately available (no button click needed)
@@ -594,13 +549,11 @@ describe('ReferencesPanel Component', () => {
594
549
 
595
550
  // Start detecting
596
551
  rerender(
597
- <EventBusProvider>
598
- <ReferencesPanel
599
- {...defaultProps}
600
- isAssisting={true}
601
- progress={{ completedEntityTypes: [] }}
602
- />
603
- </EventBusProvider>
552
+ <ReferencesPanel
553
+ {...defaultProps}
554
+ isAssisting={true}
555
+ progress={{ completedEntityTypes: [] }}
556
+ />
604
557
  );
605
558
 
606
559
  // Detecting state
@@ -622,22 +575,18 @@ describe('ReferencesPanel Component', () => {
622
575
 
623
576
  // Complete - first trigger useEffect to copy to lastDetectionLog
624
577
  rerender(
625
- <EventBusProvider>
626
- <ReferencesPanel
627
- {...defaultProps}
628
- isAssisting={false}
629
- progress={{
630
- completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
631
- }}
632
- />
633
- </EventBusProvider>
578
+ <ReferencesPanel
579
+ {...defaultProps}
580
+ isAssisting={false}
581
+ progress={{
582
+ completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
583
+ }}
584
+ />
634
585
  );
635
586
 
636
587
  // Then clear progress to show the log
637
588
  rerender(
638
- <EventBusProvider>
639
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
640
- </EventBusProvider>
589
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
641
590
  );
642
591
 
643
592
  expect(screen.queryByTestId('annotation-progress-widget')).not.toBeInTheDocument();
@@ -659,18 +608,14 @@ describe('ReferencesPanel Component', () => {
659
608
 
660
609
  // Clear progress to show the log
661
610
  rerender(
662
- <EventBusProvider>
663
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
664
- </EventBusProvider>
611
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
665
612
  );
666
613
 
667
614
  // Selection UI should be immediately available
668
615
  expect(screen.getByText('Select entity types')).toBeInTheDocument();
669
616
 
670
617
  rerender(
671
- <EventBusProvider>
672
- <ReferencesPanel {...defaultProps} />
673
- </EventBusProvider>
618
+ <ReferencesPanel {...defaultProps} />
674
619
  );
675
620
 
676
621
  expect(screen.getByText('Select entity types')).toBeInTheDocument();
@@ -2,8 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
+ import type { EventBus } from '@semiont/core';
6
+ import type { SemiontClient } from '@semiont/sdk';
5
7
  import { ResourceInfoPanel } from '../ResourceInfoPanel';
6
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
8
+ import { createTestSemiontWrapper } from '../../../../test-utils';
7
9
 
8
10
  // Mock TranslationContext
9
11
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -36,8 +38,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
36
38
  }));
37
39
 
38
40
  // Mock @semiont/api-client utilities
39
- vi.mock('@semiont/api-client', async () => {
40
- const actual = await vi.importActual('@semiont/api-client');
41
+ vi.mock('@semiont/core', async () => {
42
+ const actual = await vi.importActual('@semiont/core');
41
43
  return {
42
44
  ...actual,
43
45
  formatLocaleDisplay: vi.fn((locale: string) => `Language: ${locale}`),
@@ -61,64 +63,37 @@ interface TrackedEvent {
61
63
 
62
64
  function createEventTracker() {
63
65
  const events: TrackedEvent[] = [];
64
-
65
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
66
- const eventBus = useEventBus();
67
-
68
- React.useEffect(() => {
69
- const handlers: Array<() => void> = [];
70
-
71
- // Track resource-related events
72
- const trackEvent = (eventName: string) => (payload: any) => {
73
- events.push({ event: eventName, payload });
74
- };
75
-
76
- const resourceEvents = [
77
- 'yield:clone',
78
- 'mark:archive',
79
- 'mark:unarchive',
80
- ] as const;
81
-
82
- resourceEvents.forEach(eventName => {
83
- const handler = trackEvent(eventName);
84
- const subscription = eventBus.get(eventName).subscribe(handler);
85
- handlers.push(subscription);
86
- });
87
-
88
- return () => {
89
- handlers.forEach(sub => sub.unsubscribe());
90
- };
91
- }, [eventBus]);
92
-
93
- return <>{children}</>;
94
- }
95
-
96
66
  return {
97
- EventTrackingWrapper,
98
67
  events,
99
- clear: () => {
100
- events.length = 0;
68
+ clear: () => { events.length = 0; },
69
+ _attach(eventBus: EventBus, client: SemiontClient) {
70
+ // `yield:clone` is a local-bus UI signal emitted by `client.yield.clone()`.
71
+ eventBus.get('yield:clone').subscribe((payload: any) => {
72
+ events.push({ event: 'yield:clone', payload });
73
+ });
74
+ // `mark:archive` / `mark:unarchive` are backend-routed via `actor.emit`;
75
+ // spy on the namespace methods instead of subscribing to a local bus.
76
+ const origArchive = client.mark.archive.bind(client.mark);
77
+ const origUnarchive = client.mark.unarchive.bind(client.mark);
78
+ client.mark.archive = vi.fn(async (rid: any) => {
79
+ events.push({ event: 'mark:archive', payload: { resourceId: rid } });
80
+ return origArchive(rid);
81
+ }) as typeof client.mark.archive;
82
+ client.mark.unarchive = vi.fn(async (rid: any) => {
83
+ events.push({ event: 'mark:unarchive', payload: { resourceId: rid } });
84
+ return origUnarchive(rid);
85
+ }) as typeof client.mark.unarchive;
101
86
  },
102
87
  };
103
88
  }
104
89
 
105
- // Helper to render with EventBusProvider
106
90
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
107
- if (tracker) {
108
- return render(
109
- <EventBusProvider>
110
- <tracker.EventTrackingWrapper>
111
- {component}
112
- </tracker.EventTrackingWrapper>
113
- </EventBusProvider>
114
- );
115
- }
116
-
117
- return render(
118
- <EventBusProvider>
119
- {component}
120
- </EventBusProvider>
91
+ const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
92
+ if (tracker) tracker._attach(eventBus, client);
93
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
94
+ <SemiontWrapper>{children}</SemiontWrapper>
121
95
  );
96
+ return render(component, { wrapper: Wrapper });
122
97
  };
123
98
 
124
99
  describe('ResourceInfoPanel Component', () => {
@@ -5,14 +5,14 @@ import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../../test-utils';
6
6
  import type { components } from '@semiont/core';
7
7
 
8
- type Annotation = components['schemas']['Annotation'];
8
+ import type { Annotation } from '@semiont/core';
9
9
 
10
10
  // Stable mock functions defined outside vi.mock to avoid re-render loops
11
11
  const mockIsBodyResolved = vi.fn();
12
12
  const mockGetEntityTypes = vi.fn();
13
13
 
14
- vi.mock('@semiont/api-client', async () => {
15
- const actual = await vi.importActual('@semiont/api-client');
14
+ vi.mock('@semiont/core', async () => {
15
+ const actual = await vi.importActual('@semiont/core');
16
16
  return {
17
17
  ...actual,
18
18
  isBodyResolved: (...args: unknown[]) => mockIsBodyResolved(...args),
@@ -6,11 +6,11 @@ import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
8
 
9
- type Annotation = components['schemas']['Annotation'];
9
+ import type { Annotation } from '@semiont/core';
10
10
 
11
11
  // Mock @semiont/api-client
12
- vi.mock('@semiont/api-client', async () => {
13
- const actual = await vi.importActual('@semiont/api-client');
12
+ vi.mock('@semiont/core', async () => {
13
+ const actual = await vi.importActual('@semiont/core');
14
14
  return {
15
15
  ...actual,
16
16
  getAnnotationExactText: vi.fn(),
@@ -28,7 +28,7 @@ vi.mock('../../../../lib/tag-schemas', () => ({
28
28
  getTagSchema: vi.fn(),
29
29
  }));
30
30
 
31
- import { getAnnotationExactText } from '@semiont/api-client';
31
+ import { getAnnotationExactText } from '@semiont/core';
32
32
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
33
33
  import { getTagSchema } from '../../../../lib/tag-schemas';
34
34
  import type { MockedFunction } from 'vitest';
@@ -5,10 +5,10 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
7
7
  import { TaggingPanel } from '../TaggingPanel';
8
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
9
- import type { components } from '@semiont/core';
8
+ import type { components, EventBus } from '@semiont/core';
9
+ import { createTestSemiontWrapper } from '../../../../test-utils';
10
10
 
11
- type Annotation = components['schemas']['Annotation'];
11
+ import type { Annotation } from '@semiont/core';
12
12
 
13
13
  // Composition-based event tracker
14
14
  interface TrackedEvent {
@@ -18,59 +18,27 @@ interface TrackedEvent {
18
18
 
19
19
  function createEventTracker() {
20
20
  const events: TrackedEvent[] = [];
21
-
22
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
23
- const eventBus = useEventBus();
24
-
25
- React.useEffect(() => {
26
- const handlers: Array<() => void> = [];
27
-
28
- const trackEvent = (eventName: string) => (payload: any) => {
29
- events.push({ event: eventName, payload });
30
- };
31
-
32
- const panelEvents = ['mark:submit', 'mark:assist-request'] as const;
33
-
34
- panelEvents.forEach(eventName => {
35
- const handler = trackEvent(eventName);
36
- const subscription = eventBus.get(eventName).subscribe(handler);
37
- handlers.push(subscription);
38
- });
39
-
40
- return () => {
41
- handlers.forEach(sub => sub.unsubscribe());
42
- };
43
- }, [eventBus]);
44
-
45
- return <>{children}</>;
46
- }
47
-
48
21
  return {
49
- EventTrackingWrapper,
50
22
  events,
51
- clear: () => {
52
- events.length = 0;
23
+ clear: () => { events.length = 0; },
24
+ _attach(eventBus: EventBus) {
25
+ const panelEvents = ['mark:submit', 'mark:assist-request'] as const;
26
+ panelEvents.forEach((eventName) => {
27
+ eventBus.get(eventName).subscribe((payload: any) => {
28
+ events.push({ event: eventName, payload });
29
+ });
30
+ });
53
31
  },
54
32
  };
55
33
  }
56
34
 
57
- // Helper to render with EventBusProvider
58
35
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
59
- if (tracker) {
60
- return render(
61
- <EventBusProvider>
62
- <tracker.EventTrackingWrapper>
63
- {component}
64
- </tracker.EventTrackingWrapper>
65
- </EventBusProvider>
66
- );
67
- }
68
-
69
- return render(
70
- <EventBusProvider>
71
- {component}
72
- </EventBusProvider>
36
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
37
+ if (tracker) tracker._attach(eventBus);
38
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
39
+ <SemiontWrapper>{children}</SemiontWrapper>
73
40
  );
41
+ return render(component, { wrapper: Wrapper });
74
42
  };
75
43
 
76
44
  // Mock TranslationContext
@@ -109,8 +77,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
109
77
  }));
110
78
 
111
79
  // Mock @semiont/api-client utilities
112
- vi.mock('@semiont/api-client', async () => {
113
- const actual = await vi.importActual('@semiont/api-client');
80
+ vi.mock('@semiont/core', async () => {
81
+ const actual = await vi.importActual('@semiont/core');
114
82
  return {
115
83
  ...actual,
116
84
  getTextPositionSelector: vi.fn(),
@@ -144,8 +112,7 @@ vi.mock('../../../../lib/tag-schemas', () => ({
144
112
  ]),
145
113
  }));
146
114
 
147
- import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
148
-
115
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
149
116
  const mockGetTextPositionSelector = getTextPositionSelector as MockedFunction<typeof getTextPositionSelector>;
150
117
  const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTargetSelector>;
151
118
 
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useEffect } from 'react';
4
- import { LOCALES } from '@semiont/api-client';
4
+ import { LOCALES } from '@semiont/core';
5
5
  import { useTranslations } from '../../contexts/TranslationContext';
6
6
  import { useLanguageChangeAnnouncements } from '../LiveRegion';
7
- import { useEventBus } from '../../contexts/EventBusContext';
7
+ import { useSemiont } from '../../session/SemiontProvider';
8
8
  import './SettingsPanel.css';
9
9
 
10
10
  interface SettingsPanelProps {
@@ -31,7 +31,7 @@ export function SettingsPanel({
31
31
  hoverDelayMs
32
32
  }: SettingsPanelProps) {
33
33
  const t = useTranslations('Settings');
34
- const eventBus = useEventBus();
34
+ const semiont = useSemiont();
35
35
  const { announceLanguageChanging, announceLanguageChanged } = useLanguageChangeAnnouncements();
36
36
 
37
37
  // Track previous locale to detect changes
@@ -41,7 +41,7 @@ export function SettingsPanel({
41
41
  const handleLocaleChange = (newLocale: string) => {
42
42
  const localeName = LOCALES.find(l => l.code === newLocale)?.nativeName || newLocale;
43
43
  announceLanguageChanging(localeName);
44
- eventBus.get('settings:locale-changed').next({ locale: newLocale });
44
+ semiont.emit('settings:locale-changed', { locale: newLocale });
45
45
  };
46
46
 
47
47
  // Announce when language has successfully changed
@@ -70,7 +70,7 @@ export function SettingsPanel({
70
70
  type="button"
71
71
  role="switch"
72
72
  aria-checked={showLineNumbers}
73
- onClick={() => eventBus.get('settings:line-numbers-toggled').next(undefined)}
73
+ onClick={() => semiont.emit('settings:line-numbers-toggled', undefined)}
74
74
  className={`semiont-toggle ${
75
75
  showLineNumbers ? 'semiont-toggle--active' : ''
76
76
  }`}
@@ -94,7 +94,7 @@ export function SettingsPanel({
94
94
  </label>
95
95
  <div className="semiont-settings-panel__button-group">
96
96
  <button
97
- onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'light' })}
97
+ onClick={() => semiont.emit('settings:theme-changed', { theme: 'light' })}
98
98
  className={`semiont-panel-button ${
99
99
  theme === 'light' ? 'semiont-panel-button-active' : ''
100
100
  }`}
@@ -103,7 +103,7 @@ export function SettingsPanel({
103
103
  ☀️ {t('themeLight')}
104
104
  </button>
105
105
  <button
106
- onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'dark' })}
106
+ onClick={() => semiont.emit('settings:theme-changed', { theme: 'dark' })}
107
107
  className={`semiont-panel-button ${
108
108
  theme === 'dark' ? 'semiont-panel-button-active' : ''
109
109
  }`}
@@ -112,7 +112,7 @@ export function SettingsPanel({
112
112
  🌙 {t('themeDark')}
113
113
  </button>
114
114
  <button
115
- onClick={() => eventBus.get('settings:theme-changed').next({ theme: 'system' })}
115
+ onClick={() => semiont.emit('settings:theme-changed', { theme: 'system' })}
116
116
  className={`semiont-panel-button ${
117
117
  theme === 'system' ? 'semiont-panel-button-active' : ''
118
118
  }`}
@@ -170,7 +170,7 @@ export function SettingsPanel({
170
170
  max="500"
171
171
  step="50"
172
172
  value={hoverDelayMs}
173
- onChange={(e) => eventBus.get('settings:hover-delay-changed').next({ hoverDelayMs: Number(e.target.value) })}
173
+ onChange={(e) => semiont.emit('settings:hover-delay-changed', { hoverDelayMs: Number(e.target.value) })}
174
174
  className="semiont-slider"
175
175
  aria-describedby="hover-delay-description"
176
176
  />
@@ -13,9 +13,9 @@ vi.mock('../../LiveRegion', () => ({
13
13
  })),
14
14
  }));
15
15
 
16
- // Mock LOCALES from api-client
17
- vi.mock('@semiont/api-client', async () => {
18
- const actual = await vi.importActual('@semiont/api-client');
16
+ // Mock LOCALES (lives in @semiont/core)
17
+ vi.mock('@semiont/core', async () => {
18
+ const actual = await vi.importActual('@semiont/core');
19
19
  return {
20
20
  ...actual,
21
21
  LOCALES: [
@@ -63,12 +63,12 @@ describe('SettingsPanel', () => {
63
63
 
64
64
  it('emits settings:line-numbers-toggled on toggle click', () => {
65
65
  const handler = vi.fn();
66
- const { eventBus } = renderWithProviders(
66
+ const { shellBus } = renderWithProviders(
67
67
  <SettingsPanel {...defaultProps} />,
68
- { returnEventBus: true }
68
+ { returnShellBus: true }
69
69
  );
70
70
 
71
- const sub = eventBus!.get('settings:line-numbers-toggled').subscribe(handler);
71
+ const sub = shellBus!.get('settings:line-numbers-toggled').subscribe(handler);
72
72
  fireEvent.click(screen.getByRole('switch'));
73
73
  expect(handler).toHaveBeenCalled();
74
74
  sub.unsubscribe();
@@ -94,12 +94,12 @@ describe('SettingsPanel', () => {
94
94
 
95
95
  it('emits settings:theme-changed on theme button click', () => {
96
96
  const handler = vi.fn();
97
- const { eventBus } = renderWithProviders(
97
+ const { shellBus } = renderWithProviders(
98
98
  <SettingsPanel {...defaultProps} />,
99
- { returnEventBus: true }
99
+ { returnShellBus: true }
100
100
  );
101
101
 
102
- const sub = eventBus!.get('settings:theme-changed').subscribe(handler);
102
+ const sub = shellBus!.get('settings:theme-changed').subscribe(handler);
103
103
  fireEvent.click(screen.getByText(/Settings.themeDark/));
104
104
  expect(handler).toHaveBeenCalledWith({ theme: 'dark' });
105
105
  sub.unsubscribe();
@@ -125,12 +125,12 @@ describe('SettingsPanel', () => {
125
125
 
126
126
  it('emits settings:locale-changed on language change', () => {
127
127
  const handler = vi.fn();
128
- const { eventBus } = renderWithProviders(
128
+ const { shellBus } = renderWithProviders(
129
129
  <SettingsPanel {...defaultProps} />,
130
- { returnEventBus: true }
130
+ { returnShellBus: true }
131
131
  );
132
132
 
133
- const sub = eventBus!.get('settings:locale-changed').subscribe(handler);
133
+ const sub = shellBus!.get('settings:locale-changed').subscribe(handler);
134
134
  fireEvent.change(screen.getByLabelText('Settings.language'), {
135
135
  target: { value: 'fr' },
136
136
  });
@@ -173,12 +173,12 @@ describe('SettingsPanel', () => {
173
173
 
174
174
  it('emits settings:hover-delay-changed on slider change', () => {
175
175
  const handler = vi.fn();
176
- const { eventBus } = renderWithProviders(
176
+ const { shellBus } = renderWithProviders(
177
177
  <SettingsPanel {...defaultProps} />,
178
- { returnEventBus: true }
178
+ { returnShellBus: true }
179
179
  );
180
180
 
181
- const sub = eventBus!.get('settings:hover-delay-changed').subscribe(handler);
181
+ const sub = shellBus!.get('settings:hover-delay-changed').subscribe(handler);
182
182
  fireEvent.change(screen.getByLabelText('Settings.hoverDelay'), {
183
183
  target: { value: '300' },
184
184
  });