@semiont/react-ui 0.4.13 → 0.4.15

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 (52) hide show
  1. package/README.md +18 -12
  2. package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
  3. package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
  4. package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
  5. package/dist/chunk-OZICDVH7.mjs.map +1 -0
  6. package/dist/chunk-R2U7P4TK.mjs +865 -0
  7. package/dist/chunk-R2U7P4TK.mjs.map +1 -0
  8. package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
  9. package/dist/chunk-VN5NY4SN.mjs.map +1 -0
  10. package/dist/index.d.mts +147 -171
  11. package/dist/index.mjs +2215 -1961
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/test-utils.d.mts +13 -62
  14. package/dist/test-utils.mjs +40 -21
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +5 -3
  17. package/src/components/ProtectedErrorBoundary.tsx +95 -0
  18. package/src/components/Toolbar.tsx +13 -13
  19. package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
  20. package/src/components/modals/PermissionDeniedModal.tsx +140 -0
  21. package/src/components/modals/ReferenceWizardModal.tsx +3 -2
  22. package/src/components/modals/SessionExpiredModal.tsx +101 -0
  23. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
  24. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
  25. package/src/components/resource/AnnotationHistory.tsx +5 -6
  26. package/src/components/resource/HistoryEvent.tsx +7 -7
  27. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
  28. package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
  29. package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
  30. package/src/components/resource/event-formatting.ts +56 -56
  31. package/src/components/resource/panels/CollaborationPanel.tsx +9 -1
  32. package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
  33. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
  34. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
  35. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
  36. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
  37. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
  38. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
  39. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
  40. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
  41. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
  42. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
  43. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
  44. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
  45. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +31 -26
  46. package/src/styles/patterns/panels-base.css +12 -0
  47. package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
  48. package/dist/chunk-BQJWOK4C.mjs.map +0 -1
  49. package/dist/chunk-HNZOXH4L.mjs.map +0 -1
  50. package/dist/chunk-OL5UST25.mjs +0 -413
  51. package/dist/chunk-OL5UST25.mjs.map +0 -1
  52. /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
@@ -7,7 +7,7 @@
7
7
 
8
8
  import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
9
  import { useQueryClient } from '@tanstack/react-query';
10
- import type { components, ResourceId, ResourceEvent, GatheredContext } from '@semiont/core';
10
+ import type { components, ResourceId, GatheredContext, EventMap } from '@semiont/core';
11
11
  import { annotationId } from '@semiont/core';
12
12
  import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getMimeCategory } from '@semiont/api-client';
13
13
  import { ANNOTATORS } from '@semiont/react-ui';
@@ -90,6 +90,11 @@ export interface ResourceViewerPageProps {
90
90
  * SSE attention stream connection status for the active workspace
91
91
  */
92
92
  streamStatus: StreamStatus;
93
+
94
+ /**
95
+ * Name of the active knowledge base (for display in panels)
96
+ */
97
+ knowledgeBaseName?: string | undefined;
93
98
  }
94
99
 
95
100
  /**
@@ -128,13 +133,14 @@ export function ResourceViewerPage({
128
133
  ToolbarPanels,
129
134
  refetchDocument,
130
135
  streamStatus,
136
+ knowledgeBaseName,
131
137
  }: ResourceViewerPageProps) {
132
138
  // Translations
133
139
  const tw = useTranslations('ReferenceWizard');
134
140
 
135
141
  // Get unified event bus for subscribing to UI events
136
142
  const eventBus = useEventBus();
137
- const client = useApiClient();
143
+ const semiont = useApiClient();
138
144
  const queryClient = useQueryClient(); // retained for non-store queries (events log)
139
145
 
140
146
  // UI state hooks
@@ -158,17 +164,17 @@ export function ResourceViewerPage({
158
164
 
159
165
  // Binary path: fetch short-lived media token, construct URL
160
166
  const { token: mediaToken, loading: mediaTokenLoading } = useMediaToken(rUri);
161
- const binaryContent = (isBinary && mediaToken && client)
162
- ? `${client.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
167
+ const binaryContent = (isBinary && mediaToken && semiont)
168
+ ? `${semiont.baseUrl}/api/resources/${rUri}?token=${mediaToken}`
163
169
  : '';
164
170
 
165
171
  const content = isBinary ? binaryContent : textContent;
166
172
  const contentLoading = isBinary ? mediaTokenLoading : textLoading;
167
173
 
168
- const annotationsData = useObservable(client.stores.annotations.listForResource(rUri));
174
+ const annotationsData = useObservable(semiont.browse.annotations(rUri));
169
175
  const annotations = useMemo(
170
- () => annotationsData?.annotations || [],
171
- [annotationsData?.annotations]
176
+ () => annotationsData || [],
177
+ [annotationsData]
172
178
  );
173
179
 
174
180
  const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
@@ -203,8 +209,8 @@ export function ResourceViewerPage({
203
209
  setWizardEntityTypes(event.entityTypes);
204
210
  setWizardOpen(true);
205
211
 
206
- // Trigger context gathering
207
- eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId });
212
+ // Trigger context gathering — gather:requested is consumed by useContextGatherFlow
213
+ eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId, options: { contextWindow: 2000 } });
208
214
  });
209
215
  return () => subscription.unsubscribe();
210
216
  }, [eventBus]);
@@ -225,21 +231,18 @@ export function ResourceViewerPage({
225
231
  });
226
232
  }, [onGenerateDocument]);
227
233
 
228
- const handleWizardLinkResource = useCallback((referenceId: string, targetResourceId: string) => {
229
- eventBus.get('bind:update-body').next({
230
- annotationId: annotationId(referenceId),
231
- resourceId: rUri,
232
- operations: [{
233
- op: 'add',
234
- item: {
235
- type: 'SpecificResource' as const,
236
- source: targetResourceId,
237
- purpose: 'linking' as const,
238
- },
239
- }],
240
- });
241
- showSuccess('Reference linked successfully');
242
- }, [rUri, showSuccess]); // eventBus is stable singleton
234
+ const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
235
+ try {
236
+ await semiont.bind.body(
237
+ rUri,
238
+ annotationId(referenceId),
239
+ [{ op: 'add', item: { type: 'SpecificResource' as const, source: targetResourceId, purpose: 'linking' as const } }],
240
+ );
241
+ showSuccess('Reference linked successfully');
242
+ } catch (error) {
243
+ showError(`Failed to link reference: ${error instanceof Error ? error.message : String(error)}`);
244
+ }
245
+ }, [rUri, semiont, showSuccess, showError]);
243
246
 
244
247
  const handleWizardComposeNavigate = useCallback((
245
248
  context: GatheredContext,
@@ -358,8 +361,8 @@ export function ResourceViewerPage({
358
361
  triggerSparkleAnimation(annotationId);
359
362
  }, [triggerSparkleAnimation]);
360
363
 
361
- const handleAnnotationAdded = useCallback((event: Extract<ResourceEvent, { type: 'annotation.added' }>) => {
362
- triggerSparkleAnimation(event.payload.annotation.id);
364
+ const handleAnnotationAdded = useCallback((stored: EventMap['mark:added']) => {
365
+ triggerSparkleAnimation(stored.payload.annotation.id);
363
366
  }, [triggerSparkleAnimation]);
364
367
 
365
368
  const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
@@ -609,6 +612,7 @@ export function ResourceViewerPage({
609
612
  {/* Document Info Panel */}
610
613
  {activePanel === 'info' && (
611
614
  <ResourceInfoPanel
615
+ resourceId={rUri}
612
616
  documentEntityTypes={documentEntityTypes}
613
617
  documentLocale={getLanguage(resource)}
614
618
  primaryMediaType={primaryMediaType}
@@ -628,6 +632,7 @@ export function ResourceViewerPage({
628
632
  <CollaborationPanel
629
633
  isConnected={streamStatus === 'connected'}
630
634
  eventCount={0}
635
+ knowledgeBaseName={knowledgeBaseName}
631
636
  />
632
637
  )}
633
638
 
@@ -401,4 +401,16 @@
401
401
 
402
402
  .semiont-panel-actions--between {
403
403
  justify-content: space-between;
404
+ }
405
+
406
+ /* Login form pulsing border for first-launch nudge */
407
+ @keyframes semiont-pulse-border {
408
+ 0%, 100% { border-color: var(--semiont-color-primary-300, #93c5fd); }
409
+ 50% { border-color: var(--semiont-color-primary-600, #2563eb); }
410
+ }
411
+
412
+ .semiont-panel__login-form--pulsing {
413
+ border: 2px solid var(--semiont-color-primary-400, #60a5fa);
414
+ border-radius: var(--semiont-panel-border-radius, 0.5rem);
415
+ animation: semiont-pulse-border 2s ease-in-out infinite;
404
416
  }
@@ -1,107 +0,0 @@
1
- /**
2
- * Open Resources Manager Interface
3
- *
4
- * Manages a list of open resources (documents/files) with persistence.
5
- * This interface allows apps to provide their own implementation of resource management
6
- * (localStorage, sessionStorage, database, etc.) while components remain framework-agnostic.
7
- *
8
- * Components accept this manager as a prop instead of consuming from Context.
9
- *
10
- * @example
11
- * ```tsx
12
- * // In app (e.g., frontend/src/hooks/useOpenResourcesManager.ts)
13
- * export function useOpenResourcesManager(): OpenResourcesManager {
14
- * const [openResources, setOpenResources] = useState<OpenResource[]>([]);
15
- *
16
- * // Implementation details...
17
- *
18
- * return {
19
- * openResources,
20
- * addResource,
21
- * removeResource,
22
- * updateResourceName,
23
- * reorderResources
24
- * };
25
- * }
26
- *
27
- * // Pass to components as props
28
- * <KnowledgeNavigation openResourcesManager={openResourcesManager} />
29
- * ```
30
- */
31
- interface OpenResource {
32
- /** Unique identifier for the resource */
33
- id: string;
34
- /** Display name of the resource */
35
- name: string;
36
- /** Timestamp when the resource was opened */
37
- openedAt: number;
38
- /** Order/position for manual sorting (optional for backward compatibility) */
39
- order?: number;
40
- /** Media type for icon display (e.g., 'application/pdf', 'text/plain') */
41
- mediaType?: string;
42
- /** Working-tree URI (e.g. "file://docs/overview.md") — used as tooltip in navigation */
43
- storageUri?: string;
44
- }
45
- interface OpenResourcesManager {
46
- /** List of currently open resources */
47
- openResources: OpenResource[];
48
- /**
49
- * Add a new resource to the open list or update if already exists
50
- * @param id - Unique resource identifier
51
- * @param name - Display name of the resource
52
- * @param mediaType - Optional media type for icon display
53
- * @param storageUri - Optional working-tree URI (e.g. "file://docs/overview.md")
54
- */
55
- addResource: (id: string, name: string, mediaType?: string, storageUri?: string) => void;
56
- /**
57
- * Remove a resource from the open list
58
- * @param id - Resource identifier to remove
59
- */
60
- removeResource: (id: string) => void;
61
- /**
62
- * Update the display name of an open resource
63
- * @param id - Resource identifier
64
- * @param name - New display name
65
- */
66
- updateResourceName: (id: string, name: string) => void;
67
- /**
68
- * Reorder resources by moving from one index to another
69
- * @param oldIndex - Current position index
70
- * @param newIndex - Desired position index
71
- */
72
- reorderResources: (oldIndex: number, newIndex: number) => void;
73
- }
74
-
75
- /**
76
- * Session management interface for handling authentication state and session expiry
77
- * Apps implement this interface and pass it to SessionProvider
78
- */
79
- interface SessionState {
80
- /** Whether the user is currently authenticated */
81
- isAuthenticated: boolean;
82
- /** When the session expires (null if not authenticated) */
83
- expiresAt: Date | null;
84
- /** Time in milliseconds until session expires (null if not authenticated) */
85
- timeUntilExpiry: number | null;
86
- /** Whether the session is expiring soon (< 5 minutes) */
87
- isExpiringSoon: boolean;
88
- }
89
- interface SessionManager extends SessionState {
90
- }
91
-
92
- /**
93
- * Translation management interface
94
- * Apps implement this to provide translations using their preferred i18n library
95
- */
96
- interface TranslationManager {
97
- /**
98
- * Translate a key within a namespace
99
- * @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')
100
- * @param key - Translation key within the namespace
101
- * @param params - Optional parameters for interpolation
102
- * @returns Translated string
103
- */
104
- t: (namespace: string, key: string, params?: Record<string, any>) => string;
105
- }
106
-
107
- export type { OpenResourcesManager as O, SessionManager as S, TranslationManager as T, OpenResource as a, SessionState as b };
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/AuthTokenContext.tsx","../src/contexts/useEventSubscription.ts","../src/hooks/useBeckonFlow.ts"],"sourcesContent":["'use client';\n\n/**\n * Auth Token Context - Manages authentication token lifecycle\n *\n * Simple approach: Just pass the token value through context.\n * When the token changes, context updates, components re-render.\n * No complex machinery needed.\n */\n\nimport { createContext, useContext, ReactNode } from 'react';\n\nconst AuthTokenContext = createContext<string | null | undefined>(undefined);\n\nexport interface AuthTokenProviderProps {\n token: string | null;\n children: ReactNode;\n}\n\n/**\n * Provider for auth token\n * Pass the current token value - React handles the rest\n */\nexport function AuthTokenProvider({\n token,\n children,\n}: AuthTokenProviderProps) {\n return (\n <AuthTokenContext.Provider value={token}>\n {children}\n </AuthTokenContext.Provider>\n );\n}\n\n/**\n * Hook to get current auth token\n *\n * Returns the current token value from context.\n * Re-renders automatically when token changes (normal React behavior).\n *\n * @returns Current access token (null if not authenticated)\n * @throws Error if used outside AuthTokenProvider\n */\nexport function useAuthToken(): string | null {\n const context = useContext(AuthTokenContext);\n\n if (context === undefined) {\n throw new Error('useAuthToken must be used within an AuthTokenProvider');\n }\n\n return context;\n}\n","import { useEffect, useRef, useMemo } from 'react';\nimport type { EventMap } from '@semiont/core';\nimport { useEventBus } from './EventBusContext';\n\n/**\n * Subscribe to an event bus event with automatic cleanup.\n *\n * This hook solves the \"stale closure\" problem by always using the latest\n * version of the handler without re-subscribing.\n *\n * @example\n * ```tsx\n * useEventSubscription('mark:created', ({ annotation }) => {\n * // This always uses the latest props/state\n * triggerSparkleAnimation(annotation.id);\n * });\n * ```\n */\nexport function useEventSubscription<K extends keyof EventMap>(\n eventName: K,\n handler: (payload: EventMap[K]) => void\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handler in a ref to avoid stale closures\n const handlerRef = useRef(handler);\n\n // Update ref on every render (no re-subscription needed)\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n // Subscribe once, using a stable wrapper that calls the current handler\n useEffect(() => {\n const stableHandler = (payload: EventMap[K]) => {\n handlerRef.current(payload);\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName).subscribe(stableHandler);\n\n return () => {\n subscription.unsubscribe();\n };\n }, [eventName, eventBus]); // eventBus is stable, only re-subscribe if event name changes\n}\n\n/**\n * Subscribe to multiple events at once.\n *\n * @example\n * ```tsx\n * useEventSubscriptions({\n * 'mark:created': ({ annotation }) => setNewAnnotation(annotation),\n * 'mark:deleted': ({ annotationId }) => removeAnnotation(annotationId),\n * });\n * ```\n */\nexport function useEventSubscriptions(\n subscriptions: {\n [K in keyof EventMap]?: (payload: EventMap[K]) => void;\n }\n): void {\n const eventBus = useEventBus();\n\n // Store the latest handlers in refs\n const handlersRef = useRef(subscriptions);\n\n // Update refs on every render\n useEffect(() => {\n handlersRef.current = subscriptions;\n });\n\n // Get stable list of event names to subscribe to\n const eventNames = useMemo(\n () => Object.keys(subscriptions).sort(),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [Object.keys(subscriptions).sort().join(',')]\n );\n\n // Subscribe once per event - only re-subscribe if event names actually change\n useEffect(() => {\n const subscriptions: Array<{ unsubscribe: () => void }> = [];\n\n // Create stable wrappers for each subscription\n for (const eventName of eventNames) {\n const stableHandler = (payload: any) => {\n const currentHandler = handlersRef.current[eventName as keyof EventMap];\n if (currentHandler) {\n currentHandler(payload);\n } else {\n console.warn('[useEventSubscriptions] No current handler found for:', eventName);\n }\n };\n\n // RxJS EventBus.get() returns Subject, subscribe returns Subscription\n const subscription = eventBus.get(eventName as keyof EventMap).subscribe(stableHandler);\n subscriptions.push(subscription);\n }\n\n // Cleanup: unsubscribe from all subscriptions\n return () => {\n for (const subscription of subscriptions) {\n subscription.unsubscribe();\n }\n };\n }, [eventNames, eventBus]); // eventBus is stable singleton - never in deps; only re-subscribe if event names change\n}\n","/**\n * useBeckonFlow — Annotation attention / pointer coordination hook\n *\n * Manages which annotation currently has the user's attention:\n * - Hover state (hoveredAnnotationId)\n * - Hover → sparkle relay\n * - Click → focus relay\n *\n * Follows react-rxjs-guide.md Layer 2 pattern: Hook bridge that\n * subscribes to events and pushes values into React state.\n *\n * Note: beckon:sparkle visual effect (triggerSparkleAnimation) is owned by\n * ResourceViewerPage, which subscribes to beckon:sparkle and delegates to\n * ResourceAnnotationsContext. This hook emits the signal; it does not render the effect.\n *\n * @subscribes beckon:hover - Sets hoveredAnnotationId; emits beckon:sparkle\n * @subscribes browse:click - Emits beckon:focus (attention relay only)\n * @emits beckon:sparkle\n * @emits beckon:focus\n */\n\n/**\n * useHoverEmitter / createHoverHandlers — annotation hover emission utilities\n *\n * Centralises two hover quality-of-life behaviours:\n *\n * 1. currentHover guard — suppresses redundant emissions when the mouse\n * moves within the same annotation element (prevents event bus noise).\n *\n * 2. Debounce delay (HOVER_DELAY_MS) — a short timer before emitting\n * beckon:hover, so that transient pass-through movements (user dragging\n * the mouse across the panel to reach a button elsewhere) do not trigger\n * sparkle animations or cross-highlight effects.\n * The delay is cancelled immediately on mouseLeave, so leaving is always instant.\n *\n * Two forms are provided:\n *\n * useHoverEmitter(annotationId)\n * React hook. Returns { onMouseEnter, onMouseLeave } props for JSX elements.\n * Use in panel entries (HighlightEntry, CommentEntry, …).\n *\n * createHoverHandlers(emit)\n * Plain factory. Returns { handleMouseEnter(id), handleMouseLeave(), cleanup }.\n * Use inside useEffect / imperative setup code where hooks cannot be called\n * (BrowseView, CodeMirrorRenderer, AnnotationOverlay, PdfAnnotationCanvas).\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { accessToken } from '@semiont/core';\nimport { useEventBus } from '../contexts/EventBusContext';\nimport { useEventSubscriptions } from '../contexts/useEventSubscription';\nimport { useApiClient } from '../contexts/ApiClientContext';\nimport { useAuthToken } from '../contexts/AuthTokenContext';\nimport type { StreamStatus } from './useResourceEvents';\n\n// ─── useBeckonFlow ─────────────────────────────────────────────────────────\n\nexport interface BeckonFlowState {\n hoveredAnnotationId: string | null;\n}\n\nexport function useBeckonFlow(): BeckonFlowState {\n const eventBus = useEventBus();\n const [hoveredAnnotationId, setHoveredAnnotationId] = useState<string | null>(null);\n\n const handleAnnotationHover = useCallback(({ annotationId }: { annotationId: string | null }) => {\n setHoveredAnnotationId(annotationId);\n if (annotationId) {\n eventBus.get('beckon:sparkle').next({ annotationId });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {\n eventBus.get('beckon:focus').next({ annotationId });\n // Scroll to annotation handled by BrowseView via beckon:focus subscription\n }, []); // eventBus is stable singleton - never in deps\n\n useEventSubscriptions({\n 'beckon:hover': handleAnnotationHover,\n 'browse:click': handleAnnotationClick,\n });\n\n return { hoveredAnnotationId };\n}\n\n// ─── createHoverHandlers (use inside useEffect / imperative setup) ────────────\n\n/** Default milliseconds the mouse must dwell before beckon:hover is emitted. */\nexport const HOVER_DELAY_MS = 150;\n\ntype EmitHover = (annotationId: string | null) => void;\n\nexport interface HoverHandlers {\n /** Call with the annotation ID when the mouse enters an annotation element. */\n handleMouseEnter: (annotationId: string) => void;\n /** Call when the mouse leaves the annotation element. */\n handleMouseLeave: () => void;\n /** Cancel any pending timer — call in the useEffect cleanup. */\n cleanup: () => void;\n}\n\n/**\n * Creates hover handlers for imperative code (non-hook contexts).\n * @param emit - Callback to emit hover events\n * @param delayMs - Hover delay in milliseconds\n */\nexport function createHoverHandlers(emit: EmitHover, delayMs: number): HoverHandlers {\n let currentHover: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const cancelTimer = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const handleMouseEnter = (annotationId: string) => {\n if (currentHover === annotationId) return; // already hovering this one\n cancelTimer();\n timer = setTimeout(() => {\n timer = null;\n currentHover = annotationId;\n emit(annotationId);\n }, delayMs);\n };\n\n const handleMouseLeave = () => {\n cancelTimer();\n if (currentHover !== null) {\n currentHover = null;\n emit(null);\n }\n };\n\n return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };\n}\n\n// ─── useHoverEmitter (use in JSX onMouseEnter / onMouseLeave props) ───────────\n\nexport interface HoverEmitterProps {\n onMouseEnter: () => void;\n onMouseLeave: () => void;\n}\n\n/**\n * React hook that returns onMouseEnter / onMouseLeave props for a single\n * annotation entry element.\n *\n * @param annotationId - The ID of the annotation this element represents.\n * @param hoverDelayMs - Hover delay in milliseconds (defaults to HOVER_DELAY_MS for panel entries)\n */\nexport function useHoverEmitter(annotationId: string, hoverDelayMs: number = HOVER_DELAY_MS): HoverEmitterProps {\n const eventBus = useEventBus();\n const currentHoverRef = useRef<string | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const onMouseEnter = useCallback(() => {\n if (currentHoverRef.current === annotationId) return;\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n }\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n currentHoverRef.current = annotationId;\n eventBus.get('beckon:hover').next({ annotationId });\n }, hoverDelayMs);\n }, [annotationId, hoverDelayMs]); // eventBus is stable singleton - never in deps\n\n const onMouseLeave = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n if (currentHoverRef.current !== null) {\n currentHoverRef.current = null;\n eventBus.get('beckon:hover').next({ annotationId: null });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, []);\n\n return { onMouseEnter, onMouseLeave };\n}\n\n// ─── useAttentionStream ───────────────────────────────────────────────────────\n\n/**\n * Opens a participant-scoped SSE connection to receive cross-participant\n * beckon signals. Each incoming signal is emitted as beckon:focus on the\n * EventBus — the existing scroll/highlight machinery handles it automatically.\n *\n * Signals are ephemeral: delivered if connected, silently dropped if not.\n *\n * @example\n * ```tsx\n * // In your layout (render-nothing connector):\n * useAttentionStream();\n * ```\n */\nexport function useAttentionStream(): { status: StreamStatus } {\n const client = useApiClient();\n const token = useAuthToken();\n const tokenRef = useRef(token);\n useEffect(() => { tokenRef.current = token; });\n const [status, setStatus] = useState<StreamStatus>('disconnected');\n\n useEffect(() => {\n setStatus('connecting');\n try {\n const sub = client.flows.attentionStream(() =>\n tokenRef.current ? accessToken(tokenRef.current) : undefined\n );\n setStatus('connected');\n return () => { sub.unsubscribe(); setStatus('disconnected'); };\n } catch (error) {\n console.error('[AttentionStream] Failed to connect:', error);\n setStatus('error');\n return;\n }\n }, [client]);\n\n return { status };\n}\n"],"mappings":"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","names":["subscriptions","useRef","useEffect","useRef","useEffect"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/contexts/EventBusContext.tsx","../src/contexts/ApiClientContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useRef, type ReactNode } from 'react';\nimport { EventBus } from '@semiont/core';\n\nconst EventBusContext = createContext<EventBus | null>(null);\n\nexport interface EventBusProviderProps {\n children: ReactNode;\n}\n\n/**\n * Unified event bus provider for all application events.\n *\n * Each provider mount creates a fresh EventBus instance. This means:\n * - Workspace switches (which remount via key prop) get isolated buses\n * - Tests get isolation naturally — no resetEventBusForTesting needed\n *\n * Operation handlers (API calls triggered by events) are set up separately via\n * the useBindFlow hook, which should be called at the resource page level.\n */\nexport function EventBusProvider({ children }: EventBusProviderProps) {\n const eventBusRef = useRef<EventBus | null>(null);\n if (!eventBusRef.current) {\n eventBusRef.current = new EventBus();\n }\n const eventBus = eventBusRef.current;\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.get('beckon:hover').next({ annotationId: '123' });\n * eventBus.get('browse:sidebar-toggle').next(undefined);\n * eventBus.get('settings:theme-changed').next({ theme: 'dark' });\n *\n * // Subscribe to any event\n * useEffect(() => {\n * const unsubscribe = eventBus.on('beckon:hover', ({ annotationId }) => {\n * console.log(annotationId);\n * });\n * return () => unsubscribe();\n * }, []);\n * ```\n */\nexport function useEventBus(): EventBus {\n const eventBus = useContext(EventBusContext);\n if (!eventBus) {\n throw new Error('useEventBus must be used within EventBusProvider');\n }\n return eventBus;\n}\n","'use client';\n\nimport { createContext, useContext, ReactNode, useMemo } from 'react';\nimport { baseUrl } from '@semiont/core';\nimport { SemiontApiClient } from '@semiont/api-client';\nimport { useEventBus } from './EventBusContext';\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 — must be nested inside EventBusProvider.\n * The client is re-created when the baseUrl changes (workspace switch).\n * The EventBus is taken from EventBusContext so client and UI components\n * share the same workspace-scoped bus.\n */\nexport function ApiClientProvider({\n baseUrl: url,\n children,\n}: ApiClientProviderProps) {\n const eventBus = useEventBus();\n\n const client = useMemo(\n () => new SemiontApiClient({\n baseUrl: baseUrl(url),\n eventBus,\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, eventBus]\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"],"mappings":";;;AAEA,SAAS,eAAe,YAAY,cAA8B;AAClE,SAAS,gBAAgB;AA0BrB;AAxBJ,IAAM,kBAAkB,cAA+B,IAAI;AAgBpD,SAAS,iBAAiB,EAAE,SAAS,GAA0B;AACpE,QAAM,cAAc,OAAwB,IAAI;AAChD,MAAI,CAAC,YAAY,SAAS;AACxB,gBAAY,UAAU,IAAI,SAAS;AAAA,EACrC;AACA,QAAM,WAAW,YAAY;AAE7B,SACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,UAC9B,UACH;AAEJ;AA4BO,SAAS,cAAwB;AACtC,QAAM,WAAW,WAAW,eAAe;AAC3C,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;;;ACjEA,SAAS,iBAAAA,gBAAe,cAAAC,aAAuB,eAAe;AAC9D,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAiC7B,gBAAAC,YAAA;AA9BJ,IAAM,mBAAmBC,eAA4C,MAAS;AAavE,SAAS,kBAAkB;AAAA,EAChC,SAAS;AAAA,EACT;AACF,GAA2B;AACzB,QAAM,WAAW,YAAY;AAE7B,QAAM,SAAS;AAAA,IACb,MAAM,IAAI,iBAAiB;AAAA,MACzB,SAAS,QAAQ,GAAG;AAAA,MACpB;AAAA;AAAA,MAEA,GAAI,QAAQ,IAAI,aAAa,UAAU,EAAE,SAAS,IAAM;AAAA,IAC1D,CAAC;AAAA,IACD,CAAC,KAAK,QAAQ;AAAA,EAChB;AAEA,SACE,gBAAAD,KAAC,iBAAiB,UAAjB,EAA0B,OAAO,QAC/B,UACH;AAEJ;AAOO,SAAS,eAAiC;AAC/C,QAAM,UAAUE,YAAW,gBAAgB;AAE3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,SAAO;AACT;","names":["createContext","useContext","jsx","createContext","useContext"]}