@semiont/react-ui 0.2.33-build.80 → 0.2.33-build.82

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 (28) hide show
  1. package/dist/{EventBusContext-7GvDyO0d.d.mts → EventBusContext-CJjL_cCf.d.mts} +67 -19
  2. package/dist/{chunk-ZR4ZV2LY.mjs → chunk-QB52Q7EQ.mjs} +7 -7
  3. package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
  4. package/dist/index.d.mts +194 -208
  5. package/dist/index.mjs +1542 -1637
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/test-utils.d.mts +2 -2
  8. package/dist/test-utils.mjs +1 -1
  9. package/package.json +1 -1
  10. package/src/components/LiveRegion.tsx +18 -18
  11. package/src/components/SessionExpiryBanner.tsx +2 -3
  12. package/src/components/SessionTimer.tsx +2 -2
  13. package/src/components/__tests__/SessionTimer.test.tsx +27 -27
  14. package/src/components/resource/panels/AssessmentPanel.tsx +2 -2
  15. package/src/components/resource/panels/DetectSection.tsx +13 -7
  16. package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
  17. package/src/components/resource/panels/TaggingPanel.tsx +2 -3
  18. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -1
  19. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +16 -13
  20. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +1 -1
  21. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +13 -13
  22. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +5 -5
  23. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +7 -12
  24. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +266 -0
  25. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +11 -12
  26. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +3 -3
  27. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +132 -93
  28. package/dist/chunk-ZR4ZV2LY.mjs.map +0 -1
@@ -25,7 +25,7 @@ import { QUERY_KEYS } from '../../../lib/query-keys';
25
25
  import { useResources, useEntityTypes } from '../../../lib/api-hooks';
26
26
  import { useResourceContent } from '../../../hooks/useResourceContent';
27
27
  import { useToast } from '../../../components/Toast';
28
- import { useTheme } from '../../../hooks/useTheme';
28
+ import { useTheme } from '../../../contexts/ThemeContext';
29
29
  import { useLineNumbers } from '../../../hooks/useLineNumbers';
30
30
  import { useResourceEvents } from '../../../hooks/useResourceEvents';
31
31
  import { useDebouncedCallback } from '../../../hooks/useDebounce';
@@ -34,10 +34,13 @@ import { useOpenResources } from '../../../contexts/OpenResourcesContext';
34
34
  import { useEventBus } from '../../../contexts/EventBusContext';
35
35
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
36
36
  import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
37
+ import { useApiClient } from '../../../contexts/ApiClientContext';
38
+ import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
37
39
  import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
40
+ import { useAttentionFlow } from '../../../hooks/useAttentionFlow';
38
41
  import { usePanelNavigation } from '../../../hooks/usePanelNavigation';
39
- import { useAnnotationFlow } from '../../../hooks/useAnnotationFlow';
40
42
  import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
43
+ import { useContextRetrievalFlow } from '../../../hooks/useContextRetrievalFlow';
41
44
 
42
45
  type SemiontResource = components['schemas']['ResourceDescriptor'];
43
46
  type Annotation = components['schemas']['Annotation'];
@@ -90,6 +93,28 @@ export interface ResourceViewerPageProps {
90
93
  * ResourceViewerPage - Main component
91
94
  *
92
95
  * Uses hooks directly (NO containers, NO render props, NO ResourceViewerPageContent wrapper)
96
+ *
97
+ * @emits navigation:router-push - Navigate to a resource or filtered view
98
+ * @emits annotation:sparkle - Trigger sparkle animation on an annotation
99
+ * @emits annotation:update-body - Update annotation body content
100
+ * @subscribes resource:archive - Archive the current resource
101
+ * @subscribes resource:unarchive - Unarchive the current resource
102
+ * @subscribes resource:clone - Clone the current resource
103
+ * @subscribes annotation:sparkle - Trigger sparkle animation
104
+ * @subscribes annotation:created - Annotation was created
105
+ * @subscribes annotation:deleted - Annotation was deleted
106
+ * @subscribes annotation:create-failed - Annotation creation failed
107
+ * @subscribes annotation:delete-failed - Annotation deletion failed
108
+ * @subscribes annotation:body-updated - Annotation body was updated
109
+ * @subscribes annotation:body-update-failed - Annotation body update failed
110
+ * @subscribes settings:theme-changed - UI theme changed
111
+ * @subscribes settings:line-numbers-toggled - Line numbers display toggled
112
+ * @subscribes detection:complete - Detection completed
113
+ * @subscribes detection:failed - Detection failed
114
+ * @subscribes generation:complete - Generation completed
115
+ * @subscribes generation:failed - Generation failed
116
+ * @subscribes navigation:reference-navigate - Navigate to a referenced document
117
+ * @subscribes navigation:entity-type-clicked - Navigate filtered by entity type
93
118
  */
94
119
  export function ResourceViewerPage({
95
120
  resource,
@@ -105,6 +130,7 @@ export function ResourceViewerPage({
105
130
  }: ResourceViewerPageProps) {
106
131
  // Get unified event bus for subscribing to UI events
107
132
  const eventBus = useEventBus();
133
+ const client = useApiClient();
108
134
  const queryClient = useQueryClient();
109
135
 
110
136
  // UI state hooks
@@ -134,26 +160,25 @@ export function ResourceViewerPage({
134
160
  const allEntityTypes = (entityTypesData as { entityTypes: string[] } | undefined)?.entityTypes || [];
135
161
 
136
162
  // Flow state hooks (NO CONTAINERS)
137
- const { detectingMotivation, detectionProgress } = useDetectionFlow(rUri);
163
+ const { hoveredAnnotationId } = useAttentionFlow();
164
+ const { detectingMotivation, detectionProgress, pendingAnnotation } = useDetectionFlow(rUri);
138
165
  const { activePanel, scrollToAnnotationId, panelInitialTab, onScrollCompleted } = usePanelNavigation();
139
- const { pendingAnnotation, hoveredAnnotationId } = useAnnotationFlow(rUri);
166
+ const { searchModalOpen, pendingReferenceId, onCloseSearchModal } = useResolutionFlow(rUri);
140
167
  const {
141
168
  generationProgress,
142
169
  generationModalOpen,
143
170
  generationReferenceId,
144
171
  generationDefaultTitle,
145
- searchModalOpen,
146
- pendingReferenceId,
147
172
  onGenerateDocument,
148
173
  onCloseGenerationModal,
149
- onCloseSearchModal,
150
174
  } = useGenerationFlow(locale, rUri.split('/').pop() || '', showSuccess, showError, cacheManager, clearNewAnnotationId);
175
+ const { retrievalContext, retrievalLoading, retrievalError } = useContextRetrievalFlow(eventBus, { client, resourceUri: rUri });
151
176
 
152
177
  // Debounced invalidation for real-time events
153
178
  const debouncedInvalidateAnnotations = useDebouncedCallback(
154
179
  () => {
155
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.annotations(rUri) });
156
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.events(rUri) });
180
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
181
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
157
182
  },
158
183
  500
159
184
  );
@@ -186,7 +211,7 @@ export function ResourceViewerPage({
186
211
 
187
212
  onAnnotationBodyUpdated: useCallback((event: any) => {
188
213
  // Optimistically update annotations cache with body operations
189
- queryClient.setQueryData(QUERY_KEYS.documents.annotations(rUri), (old: any) => {
214
+ queryClient.setQueryData(QUERY_KEYS.resources.annotations(rUri), (old: any) => {
190
215
  if (!old) return old;
191
216
  return {
192
217
  ...old,
@@ -222,7 +247,7 @@ export function ResourceViewerPage({
222
247
  };
223
248
  });
224
249
 
225
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.documents.events(rUri) });
250
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
226
251
  }, [queryClient, rUri]),
227
252
 
228
253
  // Document status events
@@ -254,90 +279,103 @@ export function ResourceViewerPage({
254
279
  }, []),
255
280
  });
256
281
 
282
+ // Event handlers extracted to useCallback (tenet: no inline handlers in useEventSubscriptions)
283
+ const handleResourceArchive = useCallback(async () => {
284
+ try {
285
+ await resources.update.useMutation().mutateAsync({ rUri, data: { archived: true } });
286
+ await refetchDocument();
287
+ showSuccess('Document archived');
288
+ } catch (err) {
289
+ console.error('Failed to archive document:', err);
290
+ showError('Failed to archive document');
291
+ }
292
+ }, [resources.update, rUri, refetchDocument, showSuccess, showError]);
293
+
294
+ const handleResourceUnarchive = useCallback(async () => {
295
+ try {
296
+ await resources.update.useMutation().mutateAsync({ rUri, data: { archived: false } });
297
+ await refetchDocument();
298
+ showSuccess('Document unarchived');
299
+ } catch (err) {
300
+ console.error('Failed to unarchive document:', err);
301
+ showError('Failed to unarchive document');
302
+ }
303
+ }, [resources.update, rUri, refetchDocument, showSuccess, showError]);
304
+
305
+ const handleResourceClone = useCallback(async () => {
306
+ try {
307
+ const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
308
+ const token = result.token;
309
+ const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
310
+ await navigator.clipboard.writeText(cloneUrl);
311
+ showSuccess('Clone link copied to clipboard');
312
+ } catch (err) {
313
+ console.error('Failed to generate clone token:', err);
314
+ showError('Failed to generate clone link');
315
+ }
316
+ }, [resources.generateCloneToken, rUri, showSuccess, showError]);
317
+
318
+ const handleAnnotationSparkle = useCallback(({ annotationId }: { annotationId: string }) => {
319
+ triggerSparkleAnimation(annotationId);
320
+ }, [triggerSparkleAnimation]);
321
+
322
+ const handleAnnotationCreated = useCallback(({ annotation }: { annotation: { id: string } }) => {
323
+ triggerSparkleAnimation(annotation.id);
324
+ debouncedInvalidateAnnotations();
325
+ }, [triggerSparkleAnimation, debouncedInvalidateAnnotations]);
326
+
327
+ const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
328
+ const handleAnnotationDeleteFailed = useCallback(() => showError('Failed to delete annotation'), [showError]);
329
+ const handleAnnotationBodyUpdated = useCallback(() => {
330
+ // Success - optimistic update already applied via useResourceEvents
331
+ }, []);
332
+ const handleAnnotationBodyUpdateFailed = useCallback(() => showError('Failed to update annotation'), [showError]);
333
+
334
+ const handleSettingsThemeChanged = useCallback(({ theme }: { theme: any }) => setTheme(theme), [setTheme]);
335
+
336
+ const handleDetectionComplete = useCallback(() => {
337
+ showSuccess('Detection complete');
338
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.annotations(rUri) });
339
+ queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
340
+ }, [showSuccess, queryClient, rUri]);
341
+ const handleDetectionFailed = useCallback(() => showError('Detection failed'), [showError]);
342
+ const handleGenerationComplete = useCallback(() => showSuccess('Document generated'), [showSuccess]);
343
+ const handleGenerationFailed = useCallback(() => showError('Failed to generate document'), [showError]);
344
+
345
+ const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
346
+ if (routes.resource) {
347
+ const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
348
+ eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
349
+ }
350
+ }, [routes.resource]); // eventBus is stable singleton - never in deps
351
+
352
+ const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
353
+ if (routes.know) {
354
+ const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
355
+ eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
356
+ }
357
+ }, [routes.know]); // eventBus is stable singleton - never in deps
358
+
257
359
  // Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
258
360
  useEventSubscriptions({
259
- // Resource operations
260
- 'resource:archive': async () => {
261
- try {
262
- await resources.update.useMutation().mutateAsync({
263
- rUri,
264
- data: { archived: true }
265
- });
266
- await refetchDocument();
267
- showSuccess('Document archived');
268
- } catch (err) {
269
- console.error('Failed to archive document:', err);
270
- showError('Failed to archive document');
271
- }
272
- },
273
- 'resource:unarchive': async () => {
274
- try {
275
- await resources.update.useMutation().mutateAsync({
276
- rUri,
277
- data: { archived: false }
278
- });
279
- await refetchDocument();
280
- showSuccess('Document unarchived');
281
- } catch (err) {
282
- console.error('Failed to unarchive document:', err);
283
- showError('Failed to unarchive document');
284
- }
285
- },
286
- 'resource:clone': async () => {
287
- try {
288
- const result = await resources.generateCloneToken.useMutation().mutateAsync(rUri);
289
- const token = result.token;
290
- const cloneUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/know/clone?token=${token}`;
291
-
292
- await navigator.clipboard.writeText(cloneUrl);
293
- showSuccess('Clone link copied to clipboard');
294
- } catch (err) {
295
- console.error('Failed to generate clone token:', err);
296
- showError('Failed to generate clone link');
297
- }
298
- },
299
-
300
- // Annotation operations
301
- 'annotation:sparkle': ({ annotationId }) => {
302
- triggerSparkleAnimation(annotationId);
303
- },
304
- 'annotation:created': ({ annotation }) => {
305
- triggerSparkleAnimation(annotation.id);
306
- debouncedInvalidateAnnotations();
307
- },
361
+ 'resource:archive': handleResourceArchive,
362
+ 'resource:unarchive': handleResourceUnarchive,
363
+ 'resource:clone': handleResourceClone,
364
+ 'annotation:sparkle': handleAnnotationSparkle,
365
+ 'annotation:created': handleAnnotationCreated,
308
366
  'annotation:deleted': debouncedInvalidateAnnotations,
309
- 'annotation:create-failed': () => showError('Failed to create annotation'),
310
- 'annotation:delete-failed': () => showError('Failed to delete annotation'),
311
- 'annotation:body-updated': () => {
312
- // Success - optimistic update already applied via useResourceEvents
313
- },
314
- 'annotation:body-update-failed': () => showError('Failed to update annotation'),
315
-
316
- // Settings
317
- 'settings:theme-changed': ({ theme }) => setTheme(theme),
367
+ 'annotation:create-failed': handleAnnotationCreateFailed,
368
+ 'annotation:delete-failed': handleAnnotationDeleteFailed,
369
+ 'annotation:body-updated': handleAnnotationBodyUpdated,
370
+ 'annotation:body-update-failed': handleAnnotationBodyUpdateFailed,
371
+ 'settings:theme-changed': handleSettingsThemeChanged,
318
372
  'settings:line-numbers-toggled': toggleLineNumbers,
319
-
320
- // Detection/Generation
321
- 'detection:complete': () => showSuccess('Detection complete'),
322
- 'detection:failed': () => showError('Detection failed'),
323
- 'generation:complete': () => showSuccess('Document generated'),
324
- 'generation:failed': () => showError('Failed to generate document'),
325
-
326
- // Navigation
327
- 'navigation:reference-navigate': ({ documentId }: { documentId: string }) => {
328
- // Navigate to the referenced document
329
- if (routes.resource) {
330
- const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
331
- eventBus.emit('navigation:router-push', { path, reason: 'reference-link' });
332
- }
333
- },
334
- 'navigation:entity-type-clicked': ({ entityType }: { entityType: string }) => {
335
- // Navigate to discovery page filtered by entity type
336
- if (routes.know) {
337
- const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
338
- eventBus.emit('navigation:router-push', { path, reason: 'entity-type-filter' });
339
- }
340
- },
373
+ 'detection:complete': handleDetectionComplete,
374
+ 'detection:failed': handleDetectionFailed,
375
+ 'generation:complete': handleGenerationComplete,
376
+ 'generation:failed': handleGenerationFailed,
377
+ 'navigation:reference-navigate': handleReferenceNavigate,
378
+ 'navigation:entity-type-clicked': handleEntityTypeClicked,
341
379
  });
342
380
 
343
381
  // Resource loading announcements
@@ -597,9 +635,10 @@ export function ResourceViewerPage({
597
635
  onGenerateDocument(generationReferenceId, options);
598
636
  }
599
637
  }}
600
- referenceId={generationReferenceId || ''}
601
- resourceUri={rUri}
602
638
  defaultTitle={generationDefaultTitle}
639
+ context={retrievalContext}
640
+ contextLoading={retrievalLoading}
641
+ contextError={retrievalError}
603
642
  />
604
643
  </div>
605
644
  );
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/ApiClientContext.tsx","../src/contexts/EventBusContext.tsx","../src/contexts/SessionContext.tsx","../src/components/Toast.tsx","../src/contexts/OpenResourcesContext.tsx","../src/contexts/TranslationContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, ReactNode, useMemo } from 'react';\nimport { SemiontApiClient, baseUrl } from '@semiont/api-client';\n\nconst ApiClientContext = createContext<SemiontApiClient | undefined>(undefined);\n\nexport interface ApiClientProviderProps {\n baseUrl: string;\n children: ReactNode;\n}\n\n/**\n * Provider for API client - creates a stateless singleton client\n * The client instance never changes (no token dependency)\n * Auth tokens are passed per-request via useAuthToken() in calling code\n */\nexport function ApiClientProvider({\n baseUrl: url,\n children,\n}: ApiClientProviderProps) {\n // Client created once and never recreated (no token dependency)\n const client = useMemo(\n () => new SemiontApiClient({\n baseUrl: baseUrl(url),\n // Use no timeout in test environment to avoid AbortController issues with ky + vitest\n ...(process.env.NODE_ENV !== 'test' && { timeout: 30000 }),\n }),\n [url] // Only baseUrl in deps, token removed\n );\n\n return (\n <ApiClientContext.Provider value={client}>\n {children}\n </ApiClientContext.Provider>\n );\n}\n\n/**\n * Hook to access the stateless API client singleton\n * Must be used within an ApiClientProvider\n * @returns Stateless SemiontApiClient instance\n */\nexport function useApiClient(): SemiontApiClient {\n const context = useContext(ApiClientContext);\n\n if (context === undefined) {\n throw new Error('useApiClient must be used within an ApiClientProvider');\n }\n\n return context;\n}\n","'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport mitt from 'mitt';\nimport type { ResourceEvent } from '@semiont/core';\nimport type { components, Selector, ResourceUri } from '@semiont/api-client';\n\ntype Annotation = components['schemas']['Annotation'];\ntype Motivation = components['schemas']['Motivation'];\n\ninterface SelectionData {\n exact: string;\n start: number;\n end: number;\n svgSelector?: string;\n fragmentSelector?: string;\n conformsTo?: string;\n prefix?: string;\n suffix?: string;\n}\n\n/**\n * Unified event map for all application events\n *\n * Consolidates events from:\n * - MakeMeaningEventBus (document/annotation operations)\n * - NavigationEventBus (navigation and sidebar UI)\n * - GlobalSettingsEventBus (app-wide settings)\n */\nexport type EventMap = {\n // ===== BACKEND EVENTS (from SSE) =====\n\n // Generic event (all types)\n 'make-meaning:event': ResourceEvent;\n\n // Detection events\n 'detection:started': Extract<ResourceEvent, { type: 'job.started' }>;\n 'detection:progress': Extract<ResourceEvent, { type: 'job.progress' }>;\n 'detection:entity-found': Extract<ResourceEvent, { type: 'annotation.added' }>;\n 'detection:completed': Extract<ResourceEvent, { type: 'job.completed' }>;\n 'detection:failed': Extract<ResourceEvent, { type: 'job.failed' }>;\n\n // Annotation events (backend)\n 'annotation:added': Extract<ResourceEvent, { type: 'annotation.added' }>;\n 'annotation:removed': Extract<ResourceEvent, { type: 'annotation.removed' }>;\n 'annotation:updated': Extract<ResourceEvent, { type: 'annotation.body.updated' }>;\n\n // Entity tag events (backend)\n 'entity-tag:added': Extract<ResourceEvent, { type: 'entitytag.added' }>;\n 'entity-tag:removed': Extract<ResourceEvent, { type: 'entitytag.removed' }>;\n\n // Resource events (backend)\n 'resource:archived': Extract<ResourceEvent, { type: 'resource.archived' }>;\n 'resource:unarchived': Extract<ResourceEvent, { type: 'resource.unarchived' }>;\n\n // ===== USER INTERACTION EVENTS =====\n\n // Selection events (user highlighting text/regions)\n 'selection:comment-requested': SelectionData;\n 'selection:tag-requested': SelectionData;\n 'selection:assessment-requested': SelectionData;\n 'selection:reference-requested': SelectionData;\n\n // Unified annotation request event (all motivations)\n 'annotation:requested': {\n selector: Selector | Selector[];\n motivation: Motivation;\n };\n\n // Annotation interaction events\n 'annotation:cancel-pending': void;\n 'annotation:dom-hover': { annotationId: string | null }; // Raw DOM hover event from resource overlays (internal routing)\n 'annotation:hover': { annotationId: string | null }; // Bidirectional hover: annotation overlay ↔ panel entry\n 'annotation:click': { annotationId: string; motivation: Motivation }; // Click on annotation - includes motivation for panel coordination\n 'annotation:focus': { annotationId: string | null };\n 'annotation:sparkle': { annotationId: string };\n\n // Panel management events\n 'panel:toggle': { panel: string };\n 'panel:open': { panel: string; scrollToAnnotationId?: string; motivation?: string };\n 'panel:close': void;\n\n // View mode events\n 'view:mode-toggled': void;\n\n // Toolbar events (annotation UI controls)\n 'toolbar:selection-changed': { motivation: string | null };\n 'toolbar:click-changed': { action: string };\n 'toolbar:shape-changed': { shape: string };\n\n // Navigation events (sidebar UI)\n 'navigation:sidebar-toggle': void;\n 'navigation:resource-close': { resourceId: string };\n 'navigation:resource-reorder': { oldIndex: number; newIndex: number };\n 'navigation:link-clicked': { href: string; label?: string };\n 'navigation:router-push': { path: string; reason?: string };\n 'navigation:external-navigate': { url: string; resourceId?: string };\n 'navigation:reference-navigate': { documentId: string };\n 'navigation:entity-type-clicked': { entityType: string };\n\n // Settings events (app-wide)\n 'settings:theme-changed': { theme: 'light' | 'dark' | 'system' };\n 'settings:line-numbers-toggled': void;\n 'settings:locale-changed': { locale: string };\n\n // ===== API OPERATION EVENTS =====\n\n // Resource operations\n 'resource:archive': void;\n 'resource:unarchive': void;\n 'resource:clone': void;\n\n // Job control\n 'job:cancel-requested': { jobType: 'detection' | 'generation' };\n\n // Annotation CRUD operations\n 'annotation:create': {\n motivation: Motivation;\n selector: Selector | Selector[];\n body: any[];\n };\n 'annotation:created': { annotation: Annotation };\n 'annotation:create-failed': { error: Error };\n 'annotation:delete': { annotationId: string };\n 'annotation:deleted': { annotationId: string };\n 'annotation:delete-failed': { error: Error };\n 'annotation:update-body': {\n annotationUri: string;\n resourceId: string;\n operations: Array<{\n op: 'add' | 'remove' | 'replace';\n item?: any;\n oldItem?: any;\n newItem?: any;\n }>;\n };\n 'annotation:body-updated': { annotationUri: string };\n 'annotation:body-update-failed': { error: Error };\n\n // Detection operations\n 'detection:start': {\n motivation: Motivation;\n options: {\n instructions?: string;\n tone?: 'neutral' | 'supportive' | 'critical';\n density?: number;\n entityTypes?: string[];\n includeDescriptiveReferences?: boolean;\n schemaId?: string;\n categories?: string[];\n };\n };\n 'detection:complete': { motivation?: Motivation; resourceUri?: ResourceUri; progress?: any };\n 'detection:cancelled': void;\n 'detection:dismiss-progress': void;\n\n // Resource generation operations (unified event-driven flow)\n 'generation:start': {\n annotationUri: string;\n resourceUri: string;\n options: {\n title: string;\n prompt?: string;\n language?: string;\n temperature?: number;\n maxTokens?: number;\n context: any; // GenerationContext - required for generation\n };\n };\n 'generation:progress': any; // GenerationProgress from SSE\n 'generation:complete': { annotationUri: string; progress: any };\n 'generation:failed': { error: Error };\n 'generation:modal-open': {\n annotationUri: string;\n resourceUri: string;\n defaultTitle: string;\n };\n 'reference:create-manual': {\n annotationUri: string;\n title: string;\n entityTypes: string[];\n };\n 'reference:link': {\n annotationUri: string;\n searchTerm: string;\n };\n 'reference:search-modal-open': {\n referenceId: string;\n searchTerm: string;\n };\n};\n\nexport type EventBus = ReturnType<typeof mitt<EventMap>> & { busId: string };\n\nconst EventBusContext = createContext<EventBus | null>(null);\n\n/**\n * Generate an 8-digit hex identifier for an event bus instance\n */\nfunction generateBusId(): string {\n return Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, '0');\n}\n\n/**\n * Create an EventBus instance with logging and unique identifier\n */\nfunction createEventBus(): EventBus {\n const bus = mitt<EventMap>() as EventBus;\n const busId = generateBusId();\n\n // Add busId property\n bus.busId = busId;\n\n // Wrap emit to add logging with busId\n const originalEmit = bus.emit.bind(bus);\n bus.emit = ((eventName: any, payload?: any) => {\n console.info(`[EventBus:${busId}] emit:`, eventName, payload);\n return originalEmit(eventName, payload);\n }) as any;\n\n // Wrap on to add logging with busId\n const originalOn = bus.on.bind(bus);\n bus.on = ((eventName: any, handler: any) => {\n console.debug(`[EventBus:${busId}] subscribe:`, eventName);\n return originalOn(eventName, handler);\n }) as any;\n\n // Wrap off to add logging with busId\n const originalOff = bus.off.bind(bus);\n bus.off = ((eventName: any, handler?: any) => {\n console.debug(`[EventBus:${busId}] unsubscribe:`, eventName);\n return originalOff(eventName, handler);\n }) as any;\n\n return bus;\n}\n\n/**\n * Global singleton event bus.\n *\n * This ensures all components in the application share the same event bus instance,\n * which is critical for cross-component communication (e.g., hovering an annotation\n * in one component scrolls the panel in another component).\n *\n * FUTURE: Multi-Window Support\n * When we need to support multiple document windows (e.g., pop-out resource viewers),\n * we'll need to transition to a per-window event bus architecture:\n *\n * Option 1: Window-scoped event bus\n * - Create a new event bus for each window/portal\n * - Pass windowId or documentId to EventBusProvider\n * - Store Map<windowId, EventBus> instead of single global\n * - Components use useEventBus(windowId) to get correct bus\n *\n * Option 2: Event bus hierarchy\n * - Global event bus for app-wide events (settings, navigation)\n * - Per-document event bus for document-specific events (annotation hover)\n * - Components subscribe to both buses as needed\n *\n * Option 3: Cross-window event bridge\n * - Keep per-window buses isolated\n * - Use BroadcastChannel or postMessage for cross-window events\n * - Bridge pattern to sync certain events across windows\n *\n * For now, single global bus is correct for single-window app.\n */\nlet globalEventBus = createEventBus();\n\n/**\n * Reset the global event bus - FOR TESTING ONLY.\n *\n * Call this in test setup (beforeEach) to ensure test isolation.\n * Each test gets a fresh event bus with no lingering subscriptions.\n *\n * @example\n * ```typescript\n * beforeEach(() => {\n * resetEventBusForTesting();\n * });\n * ```\n */\nexport function resetEventBusForTesting() {\n globalEventBus = createEventBus();\n}\n\nexport interface EventBusProviderProps {\n children: ReactNode;\n // rUri and client removed - operation handlers are now set up via useEventOperations hook\n}\n\n/**\n * Unified event bus provider for all application events\n *\n * Consolidates three previous event buses:\n * - MakeMeaningEventBus (document/annotation operations)\n * - NavigationEventBus (navigation and sidebar UI)\n * - GlobalSettingsEventBus (app-wide settings)\n *\n * Benefits:\n * - Single import: useEventBus()\n * - No decision fatigue about which bus to use\n * - Easier cross-domain coordination\n * - Simpler provider hierarchy\n *\n * NOTE: This provider uses a global singleton event bus to ensure all components\n * share the same instance. Multiple providers in the tree will all reference the\n * same global bus.\n *\n * Operation handlers (API calls triggered by events) are set up separately via\n * the useEventOperations hook, which should be called at the resource page level.\n */\nexport function EventBusProvider({\n children,\n}: EventBusProviderProps) {\n const eventBus = useMemo(() => globalEventBus, []);\n\n return (\n <EventBusContext.Provider value={eventBus}>\n {children}\n </EventBusContext.Provider>\n );\n}\n\n/**\n * Hook to access the unified event bus\n *\n * Use this everywhere instead of:\n * - useMakeMeaningEvents()\n * - useNavigationEvents()\n * - useGlobalSettingsEvents()\n *\n * @example\n * ```typescript\n * const eventBus = useEventBus();\n *\n * // Emit any event\n * eventBus.emit('annotation:hover', { annotationId: '123' });\n * eventBus.emit('navigation:sidebar-toggle', undefined);\n * eventBus.emit('settings:theme-changed', { theme: 'dark' });\n *\n * // Subscribe to any event\n * useEffect(() => {\n * const handler = ({ annotationId }) => console.log(annotationId);\n * eventBus.on('annotation:hover', handler);\n * return () => eventBus.off('annotation:hover', handler);\n * }, []);\n * ```\n */\nexport function useEventBus(): EventBus {\n const bus = useContext(EventBusContext);\n if (!bus) {\n throw new Error('useEventBus must be used within EventBusProvider');\n }\n return bus;\n}\n","'use client';\n\nimport { createContext, useContext, ReactNode } from 'react';\nimport type { SessionManager } from '../types/SessionManager';\n\nconst SessionContext = createContext<SessionManager | null>(null);\n\n/**\n * Provider Pattern: Accepts SessionManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (next-auth, custom auth, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const sessionManager = useSessionManager(); // App's implementation\n *\n * <SessionProvider sessionManager={sessionManager}>\n * <App />\n * </SessionProvider>\n * ```\n */\nexport function SessionProvider({\n sessionManager,\n children\n}: {\n sessionManager: SessionManager;\n children: ReactNode;\n}) {\n return (\n <SessionContext.Provider value={sessionManager}>\n {children}\n </SessionContext.Provider>\n );\n}\n\n/**\n * Hook to access SessionManager from Context\n * Components use this hook to access session state and expiry information\n */\nexport function useSessionContext() {\n const context = useContext(SessionContext);\n if (!context) {\n throw new Error('useSessionContext must be used within SessionProvider');\n }\n return context;\n}","'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport './Toast.css';\n\nexport type ToastType = 'success' | 'error' | 'info' | 'warning';\n\nexport interface ToastMessage {\n id: string;\n message: string;\n type: ToastType;\n duration?: number;\n}\n\ninterface ToastProps {\n toast: ToastMessage;\n onClose: (id: string) => void;\n}\n\nconst icons = {\n success: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n warning: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n info: (\n <svg className=\"semiont-toast-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nfunction Toast({ toast, onClose }: ToastProps) {\n useEffect(() => {\n const timer = setTimeout(() => {\n onClose(toast.id);\n }, toast.duration || 3000);\n\n return () => clearTimeout(timer);\n }, [toast, onClose]);\n\n return (\n <div\n className=\"semiont-toast\"\n data-variant={toast.type}\n role=\"alert\"\n >\n <div className=\"semiont-toast-icon-wrapper\">{icons[toast.type]}</div>\n <p className=\"semiont-toast-message\">{toast.message}</p>\n <button\n onClick={() => onClose(toast.id)}\n className=\"semiont-toast-close\"\n aria-label=\"Close\"\n >\n <svg className=\"semiont-toast-close-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n );\n}\n\ninterface ToastContainerProps {\n toasts: ToastMessage[];\n onClose: (id: string) => void;\n}\n\nexport function ToastContainer({ toasts, onClose }: ToastContainerProps) {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n return () => setMounted(false);\n }, []);\n\n if (!mounted) return null;\n\n return createPortal(\n <div className=\"semiont-toast-container\">\n {toasts.map((toast) => (\n <Toast key={toast.id} toast={toast} onClose={onClose} />\n ))}\n </div>,\n document.body\n );\n}\n\n// Toast context and hook for global toast management\ninterface ToastContextType {\n showToast: (message: string, type?: ToastType, duration?: number) => void;\n showSuccess: (message: string, duration?: number) => void;\n showError: (message: string, duration?: number) => void;\n showWarning: (message: string, duration?: number) => void;\n showInfo: (message: string, duration?: number) => void;\n}\n\nconst ToastContext = React.createContext<ToastContextType | undefined>(undefined);\n\nexport function ToastProvider({ children }: { children: React.ReactNode }) {\n const [toasts, setToasts] = useState<ToastMessage[]>([]);\n\n const showToast = React.useCallback((message: string, type: ToastType = 'info', duration?: number) => {\n const id = Date.now().toString();\n const newToast: ToastMessage = duration !== undefined\n ? { id, message, type, duration }\n : { id, message, type };\n setToasts((prev) => [...prev, newToast]);\n }, []);\n\n const showSuccess = React.useCallback((message: string, duration?: number) => showToast(message, 'success', duration), [showToast]);\n const showError = React.useCallback((message: string, duration?: number) => showToast(message, 'error', duration), [showToast]);\n const showWarning = React.useCallback((message: string, duration?: number) => showToast(message, 'warning', duration), [showToast]);\n const showInfo = React.useCallback((message: string, duration?: number) => showToast(message, 'info', duration), [showToast]);\n\n const handleClose = React.useCallback((id: string) => {\n setToasts((prev) => prev.filter((toast) => toast.id !== id));\n }, []);\n\n const contextValue = React.useMemo(\n () => ({ showToast, showSuccess, showError, showWarning, showInfo }),\n [showToast, showSuccess, showError, showWarning, showInfo]\n );\n\n return (\n <ToastContext.Provider value={contextValue}>\n {children}\n <ToastContainer toasts={toasts} onClose={handleClose} />\n </ToastContext.Provider>\n );\n}\n\nexport function useToast() {\n const context = React.useContext(ToastContext);\n if (context === undefined) {\n throw new Error('useToast must be used within a ToastProvider');\n }\n return context;\n}","'use client';\n\nimport React, { createContext, useContext } from 'react';\nimport type { OpenResourcesManager } from '../types/OpenResourcesManager';\n\nconst OpenResourcesContext = createContext<OpenResourcesManager | undefined>(undefined);\n\n/**\n * Provider Pattern: Accepts OpenResourcesManager implementation as prop\n * and makes it available to child components via Context.\n *\n * Apps provide their own implementation (localStorage, sessionStorage, database, etc.)\n * and pass it to this provider at the root level.\n *\n * @example\n * ```tsx\n * // In app root\n * const openResourcesManager = useOpenResourcesManager(); // App's implementation\n *\n * <OpenResourcesProvider openResourcesManager={openResourcesManager}>\n * <App />\n * </OpenResourcesProvider>\n * ```\n */\nexport function OpenResourcesProvider({\n openResourcesManager,\n children\n}: {\n openResourcesManager: OpenResourcesManager;\n children: React.ReactNode;\n}) {\n return (\n <OpenResourcesContext.Provider value={openResourcesManager}>\n {children}\n </OpenResourcesContext.Provider>\n );\n}\n\n/**\n * Hook to access OpenResourcesManager from Context\n * Components use this hook to access open resources functionality\n */\nexport function useOpenResources(): OpenResourcesManager {\n const context = useContext(OpenResourcesContext);\n if (context === undefined) {\n throw new Error('useOpenResources must be used within an OpenResourcesProvider');\n }\n return context;\n}","'use client';\n\nimport { createContext, useContext, ReactNode, useState, useEffect, useMemo } from 'react';\nimport type { TranslationManager } from '../types/TranslationManager';\n\n// Static import for default English only - always needed as fallback\nimport enTranslations from '../../translations/en.json';\n\nconst TranslationContext = createContext<TranslationManager | null>(null);\n\n// Cache for dynamically loaded translations\nconst translationCache = new Map<string, any>();\n\n/**\n * Process ICU MessageFormat plural syntax\n * Supports: {count, plural, =0 {text} =1 {text} other {text}}\n */\nfunction processPluralFormat(text: string, params: Record<string, any>): string {\n // Match {paramName, plural, ...} with proper brace counting\n const pluralMatch = text.match(/\\{(\\w+),\\s*plural,\\s*/);\n if (!pluralMatch) {\n return text;\n }\n\n const paramName = pluralMatch[1];\n const count = params[paramName];\n if (count === undefined) {\n return text;\n }\n\n // Find the matching closing brace by counting\n let startPos = pluralMatch[0].length;\n let braceCount = 1; // We're inside the first {\n let endPos = startPos;\n\n for (let i = startPos; i < text.length; i++) {\n if (text[i] === '{') braceCount++;\n else if (text[i] === '}') {\n braceCount--;\n if (braceCount === 0) {\n endPos = i;\n break;\n }\n }\n }\n\n const pluralCases = text.substring(startPos, endPos);\n\n // Parse plural cases: =0 {text} =1 {text} other {text}\n const cases: Record<string, string> = {};\n const caseRegex = /(?:=(\\d+)|(\\w+))\\s*\\{([^}]+)\\}/g;\n let caseMatch;\n\n while ((caseMatch = caseRegex.exec(pluralCases)) !== null) {\n const [, exactNumber, keyword, textContent] = caseMatch;\n const key = exactNumber !== undefined ? `=${exactNumber}` : keyword;\n cases[key] = textContent;\n }\n\n // Select appropriate case\n const exactMatch = cases[`=${count}`];\n if (exactMatch !== undefined) {\n const result = exactMatch.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n const otherCase = cases['other'];\n if (otherCase !== undefined) {\n const result = otherCase.replace(/#/g, String(count));\n return text.substring(0, pluralMatch.index!) + result + text.substring(endPos + 1);\n }\n\n return text;\n}\n\n// List of available locales (can be extended without importing all files)\nexport const AVAILABLE_LOCALES = [\n 'ar', // Arabic\n 'bn', // Bengali\n 'cs', // Czech\n 'da', // Danish\n 'de', // German\n 'el', // Greek\n 'en', // English\n 'es', // Spanish\n 'fa', // Persian/Farsi\n 'fi', // Finnish\n 'fr', // French\n 'he', // Hebrew\n 'hi', // Hindi\n 'id', // Indonesian\n 'it', // Italian\n 'ja', // Japanese\n 'ko', // Korean\n 'ms', // Malay\n 'nl', // Dutch\n 'no', // Norwegian\n 'pl', // Polish\n 'pt', // Portuguese\n 'ro', // Romanian\n 'sv', // Swedish\n 'th', // Thai\n 'tr', // Turkish\n 'uk', // Ukrainian\n 'vi', // Vietnamese\n 'zh', // Chinese\n] as const;\nexport type AvailableLocale = typeof AVAILABLE_LOCALES[number];\n\n// Lazy load translations for a specific locale\nasync function loadTranslations(locale: string): Promise<any> {\n // Check cache first\n if (translationCache.has(locale)) {\n return translationCache.get(locale);\n }\n\n // English is already loaded statically\n if (locale === 'en') {\n translationCache.set('en', enTranslations);\n return enTranslations;\n }\n\n try {\n // Dynamic import for all other locales\n const translations = await import(`../../translations/${locale}.json`);\n const translationData = translations.default || translations;\n translationCache.set(locale, translationData);\n return translationData;\n } catch (error) {\n console.error(`Failed to load translations for locale: ${locale}`, error);\n // Fall back to English\n return enTranslations;\n }\n}\n\n// Default English translation manager (using static import)\nconst defaultTranslationManager: TranslationManager = {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n};\n\nexport interface TranslationProviderProps {\n /**\n * Option 1: Provide a complete TranslationManager implementation\n */\n translationManager?: TranslationManager;\n\n /**\n * Option 2: Use built-in translations by specifying a locale\n * When adding new locales, just add the JSON file and update AVAILABLE_LOCALES\n */\n locale?: string;\n\n /**\n * Loading component to show while translations are being loaded\n * Only relevant when using dynamic locale loading\n */\n loadingComponent?: ReactNode;\n\n children: ReactNode;\n}\n\n/**\n * Provider for translation management with dynamic loading\n *\n * Three modes of operation:\n * 1. No provider: Components use default English strings\n * 2. With locale prop: Dynamically loads translations for that locale\n * 3. With translationManager: Use custom translation implementation\n */\nexport function TranslationProvider({\n translationManager,\n locale,\n loadingComponent = null,\n children,\n}: TranslationProviderProps) {\n const [loadedTranslations, setLoadedTranslations] = useState<any>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n // Load translations when locale changes\n useEffect(() => {\n if (locale && !translationManager) {\n setIsLoading(true);\n loadTranslations(locale)\n .then(translations => {\n setLoadedTranslations(translations);\n setIsLoading(false);\n })\n .catch(error => {\n console.error('Failed to load translations:', error);\n setLoadedTranslations(enTranslations); // Fall back to English\n setIsLoading(false);\n });\n }\n }, [locale, translationManager]);\n\n // Create translation manager from loaded translations\n const localeManager = useMemo<TranslationManager | null>(() => {\n if (!loadedTranslations) return null;\n\n return {\n t: (namespace: string, key: string, params?: Record<string, any>) => {\n const translation = loadedTranslations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key} in locale ${locale}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n },\n };\n }, [loadedTranslations, locale]);\n\n // If custom translation manager provided, use it\n if (translationManager) {\n return (\n <TranslationContext.Provider value={translationManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // If locale provided and still loading, show loading component\n if (locale && isLoading) {\n return <>{loadingComponent}</>;\n }\n\n // If locale provided and translations loaded, use them\n if (locale && localeManager) {\n return (\n <TranslationContext.Provider value={localeManager}>\n {children}\n </TranslationContext.Provider>\n );\n }\n\n // Default: use English translations\n return (\n <TranslationContext.Provider value={defaultTranslationManager}>\n {children}\n </TranslationContext.Provider>\n );\n}\n\n/**\n * Hook to access translations within a namespace\n *\n * Works in three modes:\n * 1. Without provider: Returns default English translations\n * 2. With provider using locale: Returns dynamically loaded translations for that locale\n * 3. With custom provider: Uses the custom translation manager\n *\n * @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')\n * @returns Function to translate keys within the namespace\n */\nexport function useTranslations(namespace: string) {\n const context = useContext(TranslationContext);\n\n // If no context (no provider), use default English translations\n if (!context) {\n return (key: string, params?: Record<string, any>) => {\n const translations = enTranslations as Record<string, Record<string, string>>;\n const translation = translations[namespace]?.[key];\n\n if (!translation) {\n console.warn(`Translation not found for ${namespace}.${key}`);\n return `${namespace}.${key}`;\n }\n\n // Handle parameter interpolation and plural format\n if (params && typeof translation === 'string') {\n let result = translation;\n // First process plural format\n result = processPluralFormat(result, params);\n // Then handle simple parameter interpolation\n Object.entries(params).forEach(([paramKey, paramValue]) => {\n result = result.replace(new RegExp(`\\\\{${paramKey}\\\\}`, 'g'), String(paramValue));\n });\n return result;\n }\n\n return translation;\n };\n }\n\n // Return a function that translates keys within this namespace\n return (key: string, params?: Record<string, any>) => context.t(namespace, key, params);\n}\n\n/**\n * Hook to preload translations for a locale\n * Useful for preloading translations before navigation\n */\nexport function usePreloadTranslations() {\n return {\n preload: async (locale: string) => {\n try {\n await loadTranslations(locale);\n return true;\n } catch (error) {\n console.error(`Failed to preload translations for ${locale}:`, error);\n return false;\n }\n },\n isLoaded: (locale: string) => translationCache.has(locale),\n };\n}"],"mappings":"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","names":["createContext","useContext","useMemo","jsx","createContext","useContext","jsx","jsx","createContext","useContext","jsx","createContext","useContext","useState","useEffect","useMemo","jsx","createContext","useState","useEffect","useMemo","jsx","useContext"]}