@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.81

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 (213) hide show
  1. package/dist/EventBusContext-CJjL_cCf.d.mts +462 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-JZIO2A3B.mjs → chunk-QB52Q7EQ.mjs} +206 -146
  11. package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
  12. package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +715 -574
  38. package/dist/index.mjs +3898 -3575
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +3 -1
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/ResizeHandle.tsx +10 -4
  77. package/src/components/SessionExpiryBanner.tsx +2 -3
  78. package/src/components/SessionTimer.tsx +3 -3
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +33 -33
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -134
  100. package/src/components/resource/BrowseView.tsx +86 -166
  101. package/src/components/resource/HistoryEvent.tsx +13 -7
  102. package/src/components/resource/ResourceViewer.tsx +122 -264
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  104. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  105. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  106. package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
  107. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  108. package/src/components/resource/panels/CommentsPanel.tsx +121 -28
  109. package/src/components/resource/panels/DetectSection.css +36 -1
  110. package/src/components/resource/panels/DetectSection.tsx +49 -15
  111. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  112. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  113. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  114. package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  116. package/src/components/resource/panels/TagEntry.tsx +25 -33
  117. package/src/components/resource/panels/TaggingPanel.tsx +118 -30
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
  120. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  121. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
  122. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  123. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  124. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
  125. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  126. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
  127. package/src/components/settings/SettingsPanel.tsx +15 -12
  128. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  129. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  130. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  131. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  132. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  133. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  134. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  135. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  136. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  137. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  138. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  139. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  140. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  141. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  142. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  143. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
  144. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +234 -0
  145. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  146. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  147. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  148. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +503 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +139 -93
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +341 -524
  152. package/translations/ar.json +6 -3
  153. package/translations/bn.json +6 -3
  154. package/translations/cs.json +6 -3
  155. package/translations/da.json +6 -3
  156. package/translations/de.json +6 -3
  157. package/translations/el.json +6 -3
  158. package/translations/en.json +6 -3
  159. package/translations/es.json +6 -3
  160. package/translations/fa.json +6 -3
  161. package/translations/fi.json +6 -3
  162. package/translations/fr.json +6 -3
  163. package/translations/he.json +6 -3
  164. package/translations/hi.json +6 -3
  165. package/translations/id.json +6 -3
  166. package/translations/it.json +6 -3
  167. package/translations/ja.json +6 -3
  168. package/translations/ko.json +6 -3
  169. package/translations/ms.json +6 -3
  170. package/translations/nl.json +6 -3
  171. package/translations/no.json +6 -3
  172. package/translations/pl.json +6 -3
  173. package/translations/pt.json +6 -3
  174. package/translations/ro.json +6 -3
  175. package/translations/sv.json +6 -3
  176. package/translations/th.json +6 -3
  177. package/translations/tr.json +6 -3
  178. package/translations/uk.json +6 -3
  179. package/translations/vi.json +6 -3
  180. package/translations/zh.json +6 -3
  181. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  182. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  183. package/dist/ar-EMHEHPCJ.mjs.map +0 -1
  184. package/dist/bn-OVCI4F6X.mjs.map +0 -1
  185. package/dist/chunk-JZIO2A3B.mjs.map +0 -1
  186. package/dist/chunk-LIHZTECW.mjs.map +0 -1
  187. package/dist/cs-FAN66Q2F.mjs.map +0 -1
  188. package/dist/da-YBBIHI2O.mjs.map +0 -1
  189. package/dist/de-MAYU33LB.mjs.map +0 -1
  190. package/dist/el-MKGSWN4O.mjs.map +0 -1
  191. package/dist/es-52LHUWJD.mjs.map +0 -1
  192. package/dist/fa-FJICRANB.mjs.map +0 -1
  193. package/dist/fi-O455XFCR.mjs.map +0 -1
  194. package/dist/fr-TXIXHOOE.mjs.map +0 -1
  195. package/dist/he-JBSOX5IN.mjs.map +0 -1
  196. package/dist/hi-KGHI3XVT.mjs.map +0 -1
  197. package/dist/id-5OCPPZLO.mjs.map +0 -1
  198. package/dist/it-PNBBZSM2.mjs.map +0 -1
  199. package/dist/ja-LDD7R3TJ.mjs.map +0 -1
  200. package/dist/ko-F47ZDEY3.mjs.map +0 -1
  201. package/dist/ms-Z7LMXJWL.mjs.map +0 -1
  202. package/dist/nl-6SJFBPJ3.mjs.map +0 -1
  203. package/dist/no-YXPBPSGF.mjs.map +0 -1
  204. package/dist/pl-P4AZ2QME.mjs.map +0 -1
  205. package/dist/pt-LHWUS6U6.mjs.map +0 -1
  206. package/dist/ro-EA5J2ZON.mjs.map +0 -1
  207. package/dist/sv-DATBS3UQ.mjs.map +0 -1
  208. package/dist/th-WTFJRWPT.mjs.map +0 -1
  209. package/dist/tr-IKO3RXOX.mjs.map +0 -1
  210. package/dist/uk-CF6CTTRK.mjs.map +0 -1
  211. package/dist/vi-AJLTXPZQ.mjs.map +0 -1
  212. package/dist/zh-U3ORHHYH.mjs.map +0 -1
  213. /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -1,22 +1,77 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import type { MockedFunction } from 'vitest';
3
3
  import React from 'react';
4
- import { render, screen, fireEvent } from '@testing-library/react';
4
+ 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, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
8
9
  import type { components } from '@semiont/api-client';
9
10
 
10
11
  type Annotation = components['schemas']['Annotation'];
11
12
 
12
- // Mock MakeMeaningEventBusContext
13
- vi.mock('../../../../contexts/MakeMeaningEventBusContext', () => ({
14
- useMakeMeaningEvents: vi.fn(() => ({
15
- emit: vi.fn(),
16
- on: vi.fn(),
17
- off: vi.fn(),
18
- })),
19
- }));
13
+ // Composition-based event tracker
14
+ interface TrackedEvent {
15
+ event: string;
16
+ payload: any;
17
+ }
18
+
19
+ function createEventTracker() {
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 = ['annotation:create', 'detection:start'];
33
+
34
+ panelEvents.forEach(eventName => {
35
+ const handler = trackEvent(eventName);
36
+ eventBus.on(eventName, handler);
37
+ handlers.push(() => eventBus.off(eventName, handler));
38
+ });
39
+
40
+ return () => {
41
+ handlers.forEach(cleanup => cleanup());
42
+ };
43
+ }, [eventBus]);
44
+
45
+ return <>{children}</>;
46
+ }
47
+
48
+ return {
49
+ EventTrackingWrapper,
50
+ events,
51
+ clear: () => {
52
+ events.length = 0;
53
+ },
54
+ };
55
+ }
56
+
57
+ // Helper to render with EventBusProvider
58
+ 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>
73
+ );
74
+ };
20
75
 
21
76
  // Mock TranslationContext
22
77
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -50,6 +105,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
50
105
  }
51
106
  return result;
52
107
  }),
108
+ TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
53
109
  }));
54
110
 
55
111
  // Mock @semiont/api-client utilities
@@ -64,17 +120,8 @@ vi.mock('@semiont/api-client', async () => {
64
120
 
65
121
  // Mock TagEntry component to simplify testing
66
122
  vi.mock('../TagEntry', () => ({
67
- TagEntry: ({ tag, onClick, onTagRef, onTagHover }: any) => (
68
- <div
69
- data-testid={`tag-${tag.id}`}
70
- onClick={() => onClick()}
71
- >
72
- <button
73
- onMouseEnter={() => onTagHover?.(tag.id)}
74
- onMouseLeave={() => onTagHover?.(null)}
75
- >
76
- Hover
77
- </button>
123
+ TagEntry: ({ tag, onTagRef }: any) => (
124
+ <div data-testid={`tag-${tag.id}`}>
78
125
  <div>{tag.id}</div>
79
126
  </div>
80
127
  ),
@@ -152,13 +199,11 @@ const createPendingAnnotation = (exact: string) => ({
152
199
  describe('TaggingPanel Component', () => {
153
200
  const defaultProps = {
154
201
  annotations: mockTags.empty,
155
- onAnnotationClick: vi.fn(),
156
- onCreate: vi.fn(),
157
- focusedAnnotationId: null,
158
202
  pendingAnnotation: null,
159
203
  };
160
204
 
161
205
  beforeEach(() => {
206
+ resetEventBusForTesting();
162
207
  vi.clearAllMocks();
163
208
 
164
209
  // Mock scrollIntoView for jsdom
@@ -184,20 +229,21 @@ describe('TaggingPanel Component', () => {
184
229
 
185
230
  describe('Rendering', () => {
186
231
  it('should render panel header with title and count', () => {
187
- render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
232
+ renderWithEventBus(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
188
233
 
189
- expect(screen.getByText(/Tags/)).toBeInTheDocument();
234
+ const headings = screen.getAllByText(/Tags/);
235
+ expect(headings.length).toBeGreaterThan(0);
190
236
  expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
191
237
  });
192
238
 
193
239
  it('should show empty state when no tags', () => {
194
- render(<TaggingPanel {...defaultProps} />);
240
+ renderWithEventBus(<TaggingPanel {...defaultProps} />);
195
241
 
196
242
  expect(screen.getByText(/No tags yet/)).toBeInTheDocument();
197
243
  });
198
244
 
199
245
  it('should render all tags', () => {
200
- render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
246
+ renderWithEventBus(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
201
247
 
202
248
  expect(screen.getByTestId('tag-1')).toBeInTheDocument();
203
249
  expect(screen.getByTestId('tag-2')).toBeInTheDocument();
@@ -205,7 +251,7 @@ describe('TaggingPanel Component', () => {
205
251
  });
206
252
 
207
253
  it('should have proper panel structure', () => {
208
- const { container } = render(<TaggingPanel {...defaultProps} />);
254
+ const { container } = renderWithEventBus(<TaggingPanel {...defaultProps} />);
209
255
 
210
256
  const panel = container.firstChild as HTMLElement;
211
257
  expect(panel).toHaveClass('semiont-panel');
@@ -214,7 +260,7 @@ describe('TaggingPanel Component', () => {
214
260
 
215
261
  describe('Tag Sorting', () => {
216
262
  it('should sort tags by position in resource', () => {
217
- render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
263
+ renderWithEventBus(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
218
264
 
219
265
  const tags = screen.getAllByTestId(/tag-/);
220
266
 
@@ -228,14 +274,14 @@ describe('TaggingPanel Component', () => {
228
274
  mockGetTextPositionSelector.mockReturnValue(null);
229
275
 
230
276
  expect(() => {
231
- render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
277
+ renderWithEventBus(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
232
278
  }).not.toThrow();
233
279
  });
234
280
  });
235
281
 
236
282
  describe('Manual Tag Creation', () => {
237
283
  it('should not show tag creation form by default', () => {
238
- render(<TaggingPanel {...defaultProps} />);
284
+ renderWithEventBus(<TaggingPanel {...defaultProps} />);
239
285
 
240
286
  expect(screen.queryByText(/Create tag for selection/)).not.toBeInTheDocument();
241
287
  });
@@ -243,7 +289,7 @@ describe('TaggingPanel Component', () => {
243
289
  it('should show tag creation form when pendingAnnotation exists', () => {
244
290
  const pendingAnnotation = createPendingAnnotation('Selected text');
245
291
 
246
- render(
292
+ renderWithEventBus(
247
293
  <TaggingPanel
248
294
  {...defaultProps}
249
295
  pendingAnnotation={pendingAnnotation}
@@ -256,7 +302,7 @@ describe('TaggingPanel Component', () => {
256
302
  it('should display quoted selected text in tag creation form', () => {
257
303
  const pendingAnnotation = createPendingAnnotation('Selected text for tagging');
258
304
 
259
- render(
305
+ renderWithEventBus(
260
306
  <TaggingPanel
261
307
  {...defaultProps}
262
308
  pendingAnnotation={pendingAnnotation}
@@ -270,7 +316,7 @@ describe('TaggingPanel Component', () => {
270
316
  const longText = 'A'.repeat(150);
271
317
  const pendingAnnotation = createPendingAnnotation(longText);
272
318
 
273
- render(
319
+ renderWithEventBus(
274
320
  <TaggingPanel
275
321
  {...defaultProps}
276
322
  pendingAnnotation={pendingAnnotation}
@@ -284,7 +330,7 @@ describe('TaggingPanel Component', () => {
284
330
  it('should show schema selector in tag creation form', () => {
285
331
  const pendingAnnotation = createPendingAnnotation('Selected text');
286
332
 
287
- render(
333
+ renderWithEventBus(
288
334
  <TaggingPanel
289
335
  {...defaultProps}
290
336
  pendingAnnotation={pendingAnnotation}
@@ -298,7 +344,7 @@ describe('TaggingPanel Component', () => {
298
344
  it('should show category selector in tag creation form', () => {
299
345
  const pendingAnnotation = createPendingAnnotation('Selected text');
300
346
 
301
- render(
347
+ renderWithEventBus(
302
348
  <TaggingPanel
303
349
  {...defaultProps}
304
350
  pendingAnnotation={pendingAnnotation}
@@ -308,16 +354,16 @@ describe('TaggingPanel Component', () => {
308
354
  expect(screen.getByText(/Select category/)).toBeInTheDocument();
309
355
  });
310
356
 
311
- it('should call onCreate when category is selected', async () => {
312
- const onCreate = vi.fn();
357
+ it('should emit annotation:create event when category is selected', async () => {
358
+ const tracker = createEventTracker();
313
359
  const pendingAnnotation = createPendingAnnotation('Selected text');
314
360
 
315
- render(
361
+ renderWithEventBus(
316
362
  <TaggingPanel
317
363
  {...defaultProps}
318
364
  pendingAnnotation={pendingAnnotation}
319
- onCreate={onCreate}
320
- />
365
+ />,
366
+ tracker
321
367
  );
322
368
 
323
369
  // Find the category selector (the one in the pending annotation form)
@@ -330,13 +376,20 @@ describe('TaggingPanel Component', () => {
330
376
 
331
377
  await userEvent.selectOptions(categorySelect!, 'Issue');
332
378
 
333
- expect(onCreate).toHaveBeenCalledWith('legal-irac', 'Issue');
379
+ await waitFor(() => {
380
+ expect(tracker.events.some(e =>
381
+ e.event === 'annotation:create' &&
382
+ e.payload?.motivation === 'tagging' &&
383
+ e.payload?.body?.[0]?.value === 'Issue' &&
384
+ e.payload?.body?.[0]?.type === 'TextualBody'
385
+ )).toBe(true);
386
+ });
334
387
  });
335
388
 
336
389
  it('should have proper styling for tag creation form', () => {
337
390
  const pendingAnnotation = createPendingAnnotation('Selected text');
338
391
 
339
- const { container } = render(
392
+ const { container } = renderWithEventBus(
340
393
  <TaggingPanel
341
394
  {...defaultProps}
342
395
  pendingAnnotation={pendingAnnotation}
@@ -350,43 +403,23 @@ describe('TaggingPanel Component', () => {
350
403
  });
351
404
 
352
405
  describe('Tag Interactions', () => {
353
- it('should call onAnnotationClick when tag is clicked', () => {
354
- const onAnnotationClick = vi.fn();
355
- render(
406
+ it('should render tag entries', () => {
407
+ renderWithEventBus(
356
408
  <TaggingPanel
357
409
  {...defaultProps}
358
410
  annotations={mockTags.single}
359
- onAnnotationClick={onAnnotationClick}
360
411
  />
361
412
  );
362
413
 
363
414
  const tag = screen.getByTestId('tag-1');
364
- fireEvent.click(tag);
365
-
366
- expect(onAnnotationClick).toHaveBeenCalledWith(mockTags.single[0]);
415
+ expect(tag).toBeInTheDocument();
367
416
  });
368
417
  });
369
418
 
370
419
  describe('Tag Hover Behavior', () => {
371
- it('should call onAnnotationHover when provided', () => {
372
- const onAnnotationHover = vi.fn();
373
- render(
374
- <TaggingPanel
375
- {...defaultProps}
376
- annotations={mockTags.single}
377
- onAnnotationHover={onAnnotationHover}
378
- />
379
- );
380
-
381
- const hoverButton = screen.getByText('Hover');
382
- fireEvent.mouseEnter(hoverButton);
383
-
384
- expect(onAnnotationHover).toHaveBeenCalledWith('1');
385
- });
386
-
387
- it('should not error when onAnnotationHover is not provided', () => {
420
+ it('should render without errors', () => {
388
421
  expect(() => {
389
- render(
422
+ renderWithEventBus(
390
423
  <TaggingPanel
391
424
  {...defaultProps}
392
425
  annotations={mockTags.single}
@@ -397,11 +430,10 @@ describe('TaggingPanel Component', () => {
397
430
  });
398
431
 
399
432
  describe('Detection Section', () => {
400
- it('should render detection section when onDetect is provided and annotateMode is true', () => {
401
- render(
433
+ it('should render detection section when annotateMode is true', () => {
434
+ renderWithEventBus(
402
435
  <TaggingPanel
403
436
  {...defaultProps}
404
- onDetect={vi.fn()}
405
437
  annotateMode={true}
406
438
  />
407
439
  );
@@ -409,22 +441,10 @@ describe('TaggingPanel Component', () => {
409
441
  expect(screen.getByText(/Detect Tags/)).toBeInTheDocument();
410
442
  });
411
443
 
412
- it('should not render detection section when onDetect is not provided', () => {
413
- render(
414
- <TaggingPanel
415
- {...defaultProps}
416
- annotateMode={true}
417
- />
418
- );
419
-
420
- expect(screen.queryByText(/Detect Tags/)).not.toBeInTheDocument();
421
- });
422
-
423
444
  it('should not render detection section when annotateMode is false', () => {
424
- render(
445
+ renderWithEventBus(
425
446
  <TaggingPanel
426
447
  {...defaultProps}
427
- onDetect={vi.fn()}
428
448
  annotateMode={false}
429
449
  />
430
450
  );
@@ -433,10 +453,9 @@ describe('TaggingPanel Component', () => {
433
453
  });
434
454
 
435
455
  it('should show schema selector in detection section', () => {
436
- render(
456
+ renderWithEventBus(
437
457
  <TaggingPanel
438
458
  {...defaultProps}
439
- onDetect={vi.fn()}
440
459
  annotateMode={true}
441
460
  />
442
461
  );
@@ -446,10 +465,9 @@ describe('TaggingPanel Component', () => {
446
465
  });
447
466
 
448
467
  it('should show Select All and Deselect All buttons', () => {
449
- render(
468
+ renderWithEventBus(
450
469
  <TaggingPanel
451
470
  {...defaultProps}
452
- onDetect={vi.fn()}
453
471
  annotateMode={true}
454
472
  />
455
473
  );
@@ -459,10 +477,9 @@ describe('TaggingPanel Component', () => {
459
477
  });
460
478
 
461
479
  it('should show category checkboxes', () => {
462
- render(
480
+ renderWithEventBus(
463
481
  <TaggingPanel
464
482
  {...defaultProps}
465
- onDetect={vi.fn()}
466
483
  annotateMode={true}
467
484
  />
468
485
  );
@@ -474,10 +491,9 @@ describe('TaggingPanel Component', () => {
474
491
  });
475
492
 
476
493
  it('should disable detect button when no categories selected', () => {
477
- render(
494
+ renderWithEventBus(
478
495
  <TaggingPanel
479
496
  {...defaultProps}
480
- onDetect={vi.fn()}
481
497
  annotateMode={true}
482
498
  />
483
499
  );
@@ -487,10 +503,9 @@ describe('TaggingPanel Component', () => {
487
503
  });
488
504
 
489
505
  it('should enable detect button when categories are selected', async () => {
490
- render(
506
+ renderWithEventBus(
491
507
  <TaggingPanel
492
508
  {...defaultProps}
493
- onDetect={vi.fn()}
494
509
  annotateMode={true}
495
510
  />
496
511
  );
@@ -502,14 +517,14 @@ describe('TaggingPanel Component', () => {
502
517
  expect(detectButton).not.toBeDisabled();
503
518
  });
504
519
 
505
- it('should call onDetect with selected schema and categories', async () => {
506
- const onDetect = vi.fn();
507
- render(
520
+ it('should emit detection:start event with selected schema and categories', async () => {
521
+ const tracker = createEventTracker();
522
+ renderWithEventBus(
508
523
  <TaggingPanel
509
524
  {...defaultProps}
510
- onDetect={onDetect}
511
525
  annotateMode={true}
512
- />
526
+ />,
527
+ tracker
513
528
  );
514
529
 
515
530
  const issueCheckbox = screen.getByLabelText(/Issue/);
@@ -521,7 +536,15 @@ describe('TaggingPanel Component', () => {
521
536
  const detectButton = screen.getByRole('button', { name: /✨ Detect/i });
522
537
  await userEvent.click(detectButton);
523
538
 
524
- expect(onDetect).toHaveBeenCalledWith('legal-irac', ['Issue', 'Rule']);
539
+ await waitFor(() => {
540
+ expect(tracker.events.some(e =>
541
+ e.event === 'detection:start' &&
542
+ e.payload?.motivation === 'tagging' &&
543
+ e.payload?.options?.schemaId === 'legal-irac' &&
544
+ e.payload?.options?.categories?.includes('Issue') &&
545
+ e.payload?.options?.categories?.includes('Rule')
546
+ )).toBe(true);
547
+ });
525
548
  });
526
549
  });
527
550
 
@@ -529,7 +552,7 @@ describe('TaggingPanel Component', () => {
529
552
  it('should show Cancel button when pendingAnnotation exists', () => {
530
553
  const pendingAnnotation = createPendingAnnotation('Selected text');
531
554
 
532
- render(
555
+ renderWithEventBus(
533
556
  <TaggingPanel
534
557
  {...defaultProps}
535
558
  pendingAnnotation={pendingAnnotation}
@@ -542,17 +565,16 @@ describe('TaggingPanel Component', () => {
542
565
 
543
566
  describe('Accessibility', () => {
544
567
  it('should have proper heading structure', () => {
545
- render(<TaggingPanel {...defaultProps} />);
568
+ renderWithEventBus(<TaggingPanel {...defaultProps} />);
546
569
 
547
- const heading = screen.getByText(/Tags/);
548
- expect(heading).toHaveClass('semiont-panel-header__text');
570
+ const headings = screen.getAllByText(/Tags/);
571
+ expect(headings[0]).toHaveClass('semiont-panel-header__text');
549
572
  });
550
573
 
551
574
  it('should have proper checkbox labels', () => {
552
- render(
575
+ renderWithEventBus(
553
576
  <TaggingPanel
554
577
  {...defaultProps}
555
- onDetect={vi.fn()}
556
578
  annotateMode={true}
557
579
  />
558
580
  );
@@ -4,28 +4,31 @@ import React, { useEffect } from 'react';
4
4
  import { LOCALES } from '@semiont/api-client';
5
5
  import { useTranslations } from '../../contexts/TranslationContext';
6
6
  import { useLanguageChangeAnnouncements } from '../LiveRegion';
7
+ import { useEventBus } from '../../contexts/EventBusContext';
7
8
  import './SettingsPanel.css';
8
9
 
9
10
  interface SettingsPanelProps {
10
11
  showLineNumbers: boolean;
11
- onLineNumbersToggle: () => void;
12
12
  theme: 'light' | 'dark' | 'system';
13
- onThemeChange: (theme: 'light' | 'dark' | 'system') => void;
14
13
  locale: string;
15
- onLocaleChange: (locale: string) => void;
16
14
  isPendingLocaleChange?: boolean;
17
15
  }
18
16
 
17
+ /**
18
+ * Settings panel for application preferences
19
+ *
20
+ * @emits settings:locale-changed - Locale changed by user. Payload: { locale: string }
21
+ * @emits settings:line-numbers-toggled - Line numbers toggled on/off. Payload: undefined
22
+ * @emits settings:theme-changed - Theme changed by user. Payload: { theme: 'light' | 'dark' | 'system' }
23
+ */
19
24
  export function SettingsPanel({
20
25
  showLineNumbers,
21
- onLineNumbersToggle,
22
26
  theme,
23
- onThemeChange,
24
27
  locale,
25
- onLocaleChange,
26
28
  isPendingLocaleChange = false
27
29
  }: SettingsPanelProps) {
28
30
  const t = useTranslations('Settings');
31
+ const eventBus = useEventBus();
29
32
  const { announceLanguageChanging, announceLanguageChanged } = useLanguageChangeAnnouncements();
30
33
 
31
34
  // Track previous locale to detect changes
@@ -35,7 +38,7 @@ export function SettingsPanel({
35
38
  const handleLocaleChange = (newLocale: string) => {
36
39
  const localeName = LOCALES.find(l => l.code === newLocale)?.nativeName || newLocale;
37
40
  announceLanguageChanging(localeName);
38
- onLocaleChange(newLocale);
41
+ eventBus.emit('settings:locale-changed', { locale: newLocale });
39
42
  };
40
43
 
41
44
  // Announce when language has successfully changed
@@ -45,7 +48,7 @@ export function SettingsPanel({
45
48
  announceLanguageChanged(localeName);
46
49
  setPreviousLocale(locale);
47
50
  }
48
- }, [locale, previousLocale, isPendingLocaleChange, announceLanguageChanged]);
51
+ }, [locale, previousLocale, isPendingLocaleChange]);
49
52
 
50
53
  return (
51
54
  <div className="semiont-settings-panel">
@@ -64,7 +67,7 @@ export function SettingsPanel({
64
67
  type="button"
65
68
  role="switch"
66
69
  aria-checked={showLineNumbers}
67
- onClick={onLineNumbersToggle}
70
+ onClick={() => eventBus.emit('settings:line-numbers-toggled', undefined)}
68
71
  className={`semiont-toggle ${
69
72
  showLineNumbers ? 'semiont-toggle--active' : ''
70
73
  }`}
@@ -88,7 +91,7 @@ export function SettingsPanel({
88
91
  </label>
89
92
  <div className="semiont-settings-panel__button-group">
90
93
  <button
91
- onClick={() => onThemeChange('light')}
94
+ onClick={() => eventBus.emit('settings:theme-changed', { theme: 'light' })}
92
95
  className={`semiont-panel-button ${
93
96
  theme === 'light' ? 'semiont-panel-button-active' : ''
94
97
  }`}
@@ -97,7 +100,7 @@ export function SettingsPanel({
97
100
  ☀️ {t('themeLight')}
98
101
  </button>
99
102
  <button
100
- onClick={() => onThemeChange('dark')}
103
+ onClick={() => eventBus.emit('settings:theme-changed', { theme: 'dark' })}
101
104
  className={`semiont-panel-button ${
102
105
  theme === 'dark' ? 'semiont-panel-button-active' : ''
103
106
  }`}
@@ -106,7 +109,7 @@ export function SettingsPanel({
106
109
  🌙 {t('themeDark')}
107
110
  </button>
108
111
  <button
109
- onClick={() => onThemeChange('system')}
112
+ onClick={() => eventBus.emit('settings:theme-changed', { theme: 'system' })}
110
113
  className={`semiont-panel-button ${
111
114
  theme === 'system' ? 'semiont-panel-button-active' : ''
112
115
  }`}
@@ -34,11 +34,8 @@ const createMockProps = (overrides?: Partial<AdminDevOpsPageProps>): AdminDevOps
34
34
  },
35
35
  ],
36
36
  theme: 'light',
37
- onThemeChange: vi.fn(),
38
37
  showLineNumbers: false,
39
- onLineNumbersToggle: vi.fn(),
40
38
  activePanel: null,
41
- onPanelToggle: vi.fn(),
42
39
  translations: {
43
40
  title: 'DevOps',
44
41
  subtitle: 'System monitoring and management',
@@ -303,30 +300,6 @@ describe('AdminDevOpsPage', () => {
303
300
  );
304
301
  });
305
302
 
306
- it('passes onThemeChange to toolbar panels', () => {
307
- const onThemeChange = vi.fn();
308
- const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
309
- const props = createMockProps({ onThemeChange, ToolbarPanels });
310
- render(<AdminDevOpsPage {...props} />);
311
-
312
- expect(ToolbarPanels).toHaveBeenCalledWith(
313
- expect.objectContaining({ onThemeChange }),
314
- expect.anything()
315
- );
316
- });
317
-
318
- it('passes onLineNumbersToggle to toolbar panels', () => {
319
- const onLineNumbersToggle = vi.fn();
320
- const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
321
- const props = createMockProps({ onLineNumbersToggle, ToolbarPanels });
322
- render(<AdminDevOpsPage {...props} />);
323
-
324
- expect(ToolbarPanels).toHaveBeenCalledWith(
325
- expect.objectContaining({ onLineNumbersToggle }),
326
- expect.anything()
327
- );
328
- });
329
-
330
303
  it('passes activePanel to toolbar panels', () => {
331
304
  const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
332
305
  const props = createMockProps({ activePanel: 'settings', ToolbarPanels });
@@ -360,17 +333,6 @@ describe('AdminDevOpsPage', () => {
360
333
  );
361
334
  });
362
335
 
363
- it('passes onPanelToggle to toolbar', () => {
364
- const onPanelToggle = vi.fn();
365
- const Toolbar = vi.fn(() => <div data-testid="toolbar" />);
366
- const props = createMockProps({ onPanelToggle, Toolbar });
367
- render(<AdminDevOpsPage {...props} />);
368
-
369
- expect(Toolbar).toHaveBeenCalledWith(
370
- expect.objectContaining({ onPanelToggle }),
371
- expect.anything()
372
- );
373
- });
374
336
  });
375
337
 
376
338
  describe('Layout and Structure', () => {
@@ -430,14 +392,7 @@ describe('AdminDevOpsPage', () => {
430
392
  });
431
393
 
432
394
  it('handles all callbacks being defined', () => {
433
- const onThemeChange = vi.fn();
434
- const onLineNumbersToggle = vi.fn();
435
- const onPanelToggle = vi.fn();
436
- const props = createMockProps({
437
- onThemeChange,
438
- onLineNumbersToggle,
439
- onPanelToggle,
440
- });
395
+ const props = createMockProps();
441
396
  render(<AdminDevOpsPage {...props} />);
442
397
 
443
398
  expect(screen.getByText('DevOps')).toBeInTheDocument();
@@ -23,11 +23,8 @@ export interface AdminDevOpsPageProps {
23
23
 
24
24
  // UI state
25
25
  theme: 'light' | 'dark' | 'system';
26
- onThemeChange: (theme: 'light' | 'dark' | 'system') => void;
27
26
  showLineNumbers: boolean;
28
- onLineNumbersToggle: () => void;
29
27
  activePanel: string | null;
30
- onPanelToggle: (panel: string | null) => void;
31
28
 
32
29
  // Translations
33
30
  translations: {
@@ -49,11 +46,8 @@ export interface AdminDevOpsPageProps {
49
46
  export function AdminDevOpsPage({
50
47
  suggestedFeatures,
51
48
  theme,
52
- onThemeChange,
53
49
  showLineNumbers,
54
- onLineNumbersToggle,
55
50
  activePanel,
56
- onPanelToggle,
57
51
  translations: t,
58
52
  StatusDisplay,
59
53
  ToolbarPanels,
@@ -138,15 +132,12 @@ export function AdminDevOpsPage({
138
132
  <ToolbarPanels
139
133
  activePanel={activePanel}
140
134
  theme={theme}
141
- onThemeChange={onThemeChange}
142
135
  showLineNumbers={showLineNumbers}
143
- onLineNumbersToggle={onLineNumbersToggle}
144
136
  />
145
137
 
146
138
  <Toolbar
147
139
  context="simple"
148
140
  activePanel={activePanel}
149
- onPanelToggle={onPanelToggle}
150
141
  />
151
142
  </div>
152
143
  </div>