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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{PdfAnnotationCanvas.client-RAJRPQLU.mjs → PdfAnnotationCanvas.client-FGV33CWN.mjs} +9 -14
  2. package/dist/PdfAnnotationCanvas.client-FGV33CWN.mjs.map +1 -0
  3. package/dist/chunk-FC6SGLLT.mjs +141 -0
  4. package/dist/chunk-FC6SGLLT.mjs.map +1 -0
  5. package/dist/chunk-XS27QKGP.mjs +55 -0
  6. package/dist/chunk-XS27QKGP.mjs.map +1 -0
  7. package/dist/{chunk-QB52Q7EQ.mjs → chunk-YPYLOBA2.mjs} +31 -81
  8. package/dist/chunk-YPYLOBA2.mjs.map +1 -0
  9. package/dist/index.css +16 -0
  10. package/dist/index.css.map +1 -1
  11. package/dist/index.d.mts +70 -28
  12. package/dist/index.mjs +564 -621
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/test-utils.mjs +5 -3
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +1 -1
  17. package/src/components/CodeMirrorRenderer.tsx +8 -8
  18. package/src/components/annotation/AnnotateToolbar.tsx +4 -1
  19. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -17
  20. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -17
  21. package/src/components/resource/BrowseView.tsx +8 -8
  22. package/src/components/resource/__tests__/BrowseView.test.tsx +20 -12
  23. package/src/components/resource/panels/AssessmentEntry.tsx +3 -6
  24. package/src/components/resource/panels/CommentEntry.tsx +3 -6
  25. package/src/components/resource/panels/HighlightEntry.tsx +3 -6
  26. package/src/components/resource/panels/ReferenceEntry.tsx +3 -6
  27. package/src/components/resource/panels/TagEntry.tsx +3 -6
  28. package/src/components/resource/panels/TaggingPanel.tsx +5 -0
  29. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +42 -4
  30. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +44 -0
  31. package/src/components/toolbar/Toolbar.css +20 -0
  32. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +312 -0
  33. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +4 -8
  34. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +266 -0
  35. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +5 -3
  36. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +0 -1
  37. package/dist/chunk-QB52Q7EQ.mjs.map +0 -1
@@ -1,5 +1,9 @@
1
1
  'use client';
2
2
  "use client";
3
+ import {
4
+ createHoverHandlers
5
+ } from "./chunk-FC6SGLLT.mjs";
6
+ import "./chunk-XS27QKGP.mjs";
3
7
  import "./chunk-3JTO27MH.mjs";
4
8
 
5
9
  // src/components/pdf-annotation/PdfAnnotationCanvas.tsx
@@ -172,7 +176,6 @@ function PdfAnnotationCanvas({
172
176
  const [selection, setSelection] = useState(null);
173
177
  const containerRef = useRef(null);
174
178
  const imageRef = useRef(null);
175
- const currentHover = useRef(null);
176
179
  useEffect(() => {
177
180
  let cancelled = false;
178
181
  async function loadPdf() {
@@ -354,18 +357,10 @@ function PdfAnnotationCanvas({
354
357
  const page = getPageFromFragment(fragmentSel.value);
355
358
  return page === pageNumber;
356
359
  });
357
- const handleMouseEnter = (annotationId) => {
358
- if (currentHover.current !== annotationId) {
359
- currentHover.current = annotationId;
360
- eventBus?.emit("annotation:hover", { annotationId });
361
- }
362
- };
363
- const handleMouseLeave = () => {
364
- if (currentHover.current !== null) {
365
- currentHover.current = null;
366
- eventBus?.emit("annotation:hover", { annotationId: null });
367
- }
368
- };
360
+ const { handleMouseEnter, handleMouseLeave } = useMemo(
361
+ () => createHoverHandlers((annotationId) => eventBus?.emit("annotation:hover", { annotationId })),
362
+ [eventBus]
363
+ );
369
364
  const { stroke, fill } = getMotivationColor(selectedMotivation ?? null);
370
365
  if (error) {
371
366
  return /* @__PURE__ */ jsx("div", { className: "semiont-pdf-annotation-canvas__error", children: error });
@@ -510,4 +505,4 @@ function PdfAnnotationCanvas({
510
505
  export {
511
506
  PdfAnnotationCanvas
512
507
  };
513
- //# sourceMappingURL=PdfAnnotationCanvas.client-RAJRPQLU.mjs.map
508
+ //# sourceMappingURL=PdfAnnotationCanvas.client-FGV33CWN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/pdf-annotation/PdfAnnotationCanvas.tsx","../src/lib/pdf-coordinates.ts","../src/lib/browser-pdfjs.ts"],"sourcesContent":["'use client';\n\nimport React, { useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport { createHoverHandlers } from '../../hooks/useAttentionFlow';\nimport type { components, ResourceUri } from '@semiont/api-client';\nimport { getTargetSelector } from '@semiont/api-client';\nimport type { SelectionMotivation } from '../annotation/AnnotateToolbar';\nimport type { EventBus } from '../../contexts/EventBusContext';\nimport {\n canvasToPdfCoordinates,\n pdfToCanvasCoordinates,\n createFragmentSelector,\n parseFragmentSelector,\n getPageFromFragment,\n type CanvasRectangle\n} from '../../lib/pdf-coordinates';\nimport {\n loadPdfDocument,\n renderPdfPageToDataUrl,\n type PDFDocumentProxy\n} from '../../lib/browser-pdfjs';\nimport './PdfAnnotationCanvas.css';\n\ntype Annotation = components['schemas']['Annotation'];\n\nexport type DrawingMode = 'rectangle' | 'circle' | 'polygon' | null;\n\n/**\n * Get color for annotation based on motivation\n */\nfunction getMotivationColor(motivation: SelectionMotivation | null): { stroke: string; fill: string } {\n if (!motivation) {\n return { stroke: 'rgb(156, 163, 175)', fill: 'rgba(156, 163, 175, 0.2)' };\n }\n\n switch (motivation) {\n case 'highlighting':\n return { stroke: 'rgb(250, 204, 21)', fill: 'rgba(250, 204, 21, 0.3)' };\n case 'linking':\n return { stroke: 'rgb(59, 130, 246)', fill: 'rgba(59, 130, 246, 0.2)' };\n case 'assessing':\n return { stroke: 'rgb(239, 68, 68)', fill: 'rgba(239, 68, 68, 0.2)' };\n case 'commenting':\n return { stroke: 'rgb(255, 255, 255)', fill: 'rgba(255, 255, 255, 0.2)' };\n default:\n return { stroke: 'rgb(156, 163, 175)', fill: 'rgba(156, 163, 175, 0.2)' };\n }\n}\n\ninterface PdfAnnotationCanvasProps {\n resourceUri: ResourceUri;\n existingAnnotations?: Annotation[];\n drawingMode: DrawingMode;\n selectedMotivation?: SelectionMotivation | null;\n eventBus?: EventBus;\n hoveredAnnotationId?: string | null;\n selectedAnnotationId?: string | null;\n}\n\n/**\n * PDF annotation canvas with page navigation and rectangle drawing\n *\n * @emits annotation:click - Annotation clicked on PDF. Payload: { annotationId: string, motivation: Motivation }\n * @emits annotation:requested - New annotation drawn on PDF. Payload: { selector: FragmentSelector, motivation: SelectionMotivation }\n * @emits annotation:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }\n */\nexport function PdfAnnotationCanvas({\n resourceUri,\n existingAnnotations = [],\n drawingMode,\n selectedMotivation,\n eventBus,\n hoveredAnnotationId,\n selectedAnnotationId\n}: PdfAnnotationCanvasProps) {\n const pdfUrl = useMemo(() => {\n const resourceId = resourceUri.split('/').pop();\n return `/api/resources/${resourceId}`;\n }, [resourceUri]);\n\n // Removed excessive logging\n\n // PDF state\n const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);\n const [numPages, setNumPages] = useState<number>(0);\n const [pageNumber, setPageNumber] = useState(1);\n const [pageImageUrl, setPageImageUrl] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [pageDimensions, setPageDimensions] = useState<{ width: number; height: number } | null>(null);\n const [displayDimensions, setDisplayDimensions] = useState<{ width: number; height: number } | null>(null);\n const [scale] = useState(1.5); // Fixed scale for better quality\n\n // Drawing state\n const [isDrawing, setIsDrawing] = useState(false);\n const [selection, setSelection] = useState<CanvasRectangle | null>(null);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const imageRef = useRef<HTMLImageElement>(null);\n\n // Load PDF document on mount\n useEffect(() => {\n let cancelled = false;\n\n async function loadPdf() {\n try {\n setIsLoading(true);\n setError(null);\n\n const doc = await loadPdfDocument(pdfUrl);\n\n if (cancelled) return;\n\n setPdfDoc(doc);\n setNumPages(doc.numPages);\n setIsLoading(false);\n } catch (err) {\n if (cancelled) return;\n\n console.error('Error loading PDF:', err);\n setError('Failed to load PDF');\n setIsLoading(false);\n }\n }\n\n loadPdf();\n\n return () => {\n cancelled = true;\n };\n }, [pdfUrl]);\n\n // Load current page when page number changes\n useEffect(() => {\n if (!pdfDoc) return;\n\n let cancelled = false;\n const doc = pdfDoc;\n\n async function loadPage() {\n try {\n const page = await doc.getPage(pageNumber);\n\n if (cancelled) return;\n\n // Get page dimensions (at scale 1.0)\n const viewport = page.getViewport({ scale: 1.0 });\n setPageDimensions({\n width: viewport.width,\n height: viewport.height\n });\n\n // Render page to image\n const { dataUrl } = await renderPdfPageToDataUrl(page, scale);\n\n if (cancelled) return;\n\n setPageImageUrl(dataUrl);\n } catch (err) {\n if (cancelled) return;\n\n console.error('Error loading page:', err);\n setError('Failed to load page');\n }\n }\n\n loadPage();\n\n return () => {\n cancelled = true;\n };\n }, [pdfDoc, pageNumber, scale]);\n\n // Update display dimensions on resize\n useEffect(() => {\n const updateDisplayDimensions = () => {\n if (imageRef.current) {\n setDisplayDimensions({\n width: imageRef.current.clientWidth,\n height: imageRef.current.clientHeight\n });\n }\n };\n\n updateDisplayDimensions();\n\n // Use ResizeObserver to detect image element size changes\n // This catches: sidebar open/close, window resize, font size changes, etc.\n let resizeObserver: ResizeObserver | null = null;\n\n try {\n resizeObserver = new ResizeObserver(updateDisplayDimensions);\n if (imageRef.current) {\n resizeObserver.observe(imageRef.current);\n }\n } catch (error) {\n // Fallback for browsers without ResizeObserver support\n console.warn('ResizeObserver not supported, falling back to window resize listener');\n window.addEventListener('resize', updateDisplayDimensions);\n }\n\n return () => {\n if (resizeObserver) {\n resizeObserver.disconnect();\n } else {\n window.removeEventListener('resize', updateDisplayDimensions);\n }\n };\n }, [pageImageUrl]);\n\n // Mouse event handlers for drawing\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n if (!drawingMode) return;\n if (!imageRef.current) return;\n\n const rect = imageRef.current.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Clear any previous selection when starting new drawing\n setIsDrawing(true);\n setSelection({\n startX: x,\n startY: y,\n endX: x,\n endY: y\n });\n }, [drawingMode]);\n\n const handleMouseMove = useCallback((e: React.MouseEvent) => {\n if (!isDrawing || !selection || !imageRef.current) return;\n\n const rect = imageRef.current.getBoundingClientRect();\n\n setSelection({\n ...selection,\n endX: e.clientX - rect.left,\n endY: e.clientY - rect.top\n });\n }, [isDrawing, selection]);\n\n const handleMouseUp = useCallback(() => {\n if (!isDrawing || !selection || !pageDimensions || !displayDimensions || !eventBus) {\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n\n // Calculate drag distance\n const dragDistance = Math.sqrt(\n Math.pow(selection.endX - selection.startX, 2) +\n Math.pow(selection.endY - selection.startY, 2)\n );\n\n // Minimum drag threshold in pixels (10px)\n const MIN_DRAG_DISTANCE = 10;\n\n if (dragDistance < MIN_DRAG_DISTANCE) {\n // This was a click, not a drag - check if we clicked an existing annotation\n if (existingAnnotations.length > 0) {\n const clickedAnnotation = pageAnnotations.find(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return false;\n\n const pdfCoord = parseFragmentSelector(fragmentSel.value);\n if (!pdfCoord) return false;\n\n const rect = pdfToCanvasCoordinates(pdfCoord, pageDimensions.height, 1.0);\n\n // Scale to display coordinates\n const scaleX = displayDimensions.width / pageDimensions.width;\n const scaleY = displayDimensions.height / pageDimensions.height;\n\n const displayX = rect.x * scaleX;\n const displayY = rect.y * scaleY;\n const displayWidth = rect.width * scaleX;\n const displayHeight = rect.height * scaleY;\n\n return (\n selection.endX >= displayX &&\n selection.endX <= displayX + displayWidth &&\n selection.endY >= displayY &&\n selection.endY <= displayY + displayHeight\n );\n });\n\n if (clickedAnnotation) {\n eventBus?.emit('annotation:click', { annotationId: clickedAnnotation.id, motivation: clickedAnnotation.motivation });\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n }\n\n // Click on empty space - do nothing\n setIsDrawing(false);\n setSelection(null);\n return;\n }\n\n // This was a drag - create new annotation\n // Scale selection from display coordinates to native page coordinates\n const scaleX = pageDimensions.width / displayDimensions.width;\n const scaleY = pageDimensions.height / displayDimensions.height;\n\n const nativeSelection: CanvasRectangle = {\n startX: selection.startX * scaleX,\n startY: selection.startY * scaleY,\n endX: selection.endX * scaleX,\n endY: selection.endY * scaleY\n };\n\n // Convert canvas coordinates to PDF coordinates\n const pdfCoord = canvasToPdfCoordinates(\n nativeSelection,\n pageNumber,\n pageDimensions.width,\n pageDimensions.height,\n 1.0 // Use scale 1.0 since we already scaled to native coords\n );\n\n // Create FragmentSelector\n const fragmentSelector = createFragmentSelector(pdfCoord);\n\n // Emit annotation:requested event with FragmentSelector\n if (selectedMotivation) {\n eventBus.emit('annotation:requested', {\n selector: {\n type: 'FragmentSelector',\n conformsTo: 'http://tools.ietf.org/rfc/rfc3778',\n value: fragmentSelector\n },\n motivation: selectedMotivation\n });\n }\n\n // Keep drawing state active to show preview until annotation is persisted\n // The parent component should clear this by changing drawingMode after save\n setIsDrawing(false);\n // Note: We keep selection so the preview remains visible\n // It will be cleared when drawingMode changes or user starts new selection\n }, [isDrawing, selection, pageNumber, pageDimensions, displayDimensions, selectedMotivation, existingAnnotations]);\n\n // Helper to get FragmentSelector from annotation target\n const getFragmentSelector = (target: Annotation['target']) => {\n const selector = getTargetSelector(target);\n if (!selector) return null;\n const selectors = Array.isArray(selector) ? selector : [selector];\n\n const found = selectors.find(s => s.type === 'FragmentSelector');\n if (!found || found.type !== 'FragmentSelector') return null;\n return found as { type: 'FragmentSelector'; value: string; conformsTo?: string };\n };\n\n // Filter annotations for current page\n const pageAnnotations = existingAnnotations.filter(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return false;\n const page = getPageFromFragment(fragmentSel.value);\n return page === pageNumber;\n });\n\n // Hover handlers with currentHover guard and dwell delay\n const { handleMouseEnter, handleMouseLeave } = useMemo(\n () => createHoverHandlers((annotationId) => eventBus?.emit('annotation:hover', { annotationId })),\n [eventBus]\n );\n\n // Calculate motivation color\n const { stroke, fill } = getMotivationColor(selectedMotivation ?? null);\n\n if (error) {\n return <div className=\"semiont-pdf-annotation-canvas__error\">{error}</div>;\n }\n\n return (\n <div className=\"semiont-pdf-annotation-canvas\">\n {isLoading && <div className=\"semiont-pdf-annotation-canvas__loading\">Loading PDF...</div>}\n\n <div\n ref={containerRef}\n className=\"semiont-pdf-annotation-canvas__container\"\n style={{ display: isLoading ? 'none' : undefined }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onMouseLeave={() => {\n if (isDrawing) {\n setIsDrawing(false);\n setSelection(null);\n }\n }}\n data-drawing-mode={drawingMode || 'none'}\n >\n {/* PDF page rendered as image */}\n {pageImageUrl && (\n <img\n ref={imageRef}\n src={pageImageUrl}\n alt={`PDF page ${pageNumber}`}\n className=\"semiont-pdf-annotation-canvas__image\"\n draggable={false}\n style={{ pointerEvents: 'none' }}\n onLoad={() => {\n // Use double RAF to ensure layout is complete even in onLoad\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (imageRef.current) {\n setDisplayDimensions({\n width: imageRef.current.clientWidth,\n height: imageRef.current.clientHeight\n });\n }\n });\n });\n }}\n />\n )}\n\n {/* SVG overlay for annotations */}\n {displayDimensions && pageDimensions && (\n <div className=\"semiont-pdf-annotation-canvas__overlay-container\">\n <div className=\"semiont-pdf-annotation-canvas__overlay\">\n <svg\n className=\"semiont-pdf-annotation-canvas__svg\"\n width={displayDimensions.width}\n height={displayDimensions.height}\n >\n {/* Render existing annotations for this page */}\n {pageAnnotations.map(ann => {\n const fragmentSel = getFragmentSelector(ann.target);\n if (!fragmentSel) return null;\n\n const pdfCoord = parseFragmentSelector(fragmentSel.value);\n if (!pdfCoord) return null;\n\n const rect = pdfToCanvasCoordinates(pdfCoord, pageDimensions.height, 1.0);\n\n // Scale to display coordinates\n const scaleX = displayDimensions.width / pageDimensions.width;\n const scaleY = displayDimensions.height / pageDimensions.height;\n\n const isHovered = ann.id === hoveredAnnotationId;\n const isSelected = ann.id === selectedAnnotationId;\n\n // Get color for this annotation's motivation (not the selected motivation)\n const annMotivation = ann.motivation as SelectionMotivation | null;\n const { stroke: annStroke, fill: annFill } = getMotivationColor(annMotivation);\n\n return (\n <rect\n key={ann.id}\n x={rect.x * scaleX}\n y={rect.y * scaleY}\n width={rect.width * scaleX}\n height={rect.height * scaleY}\n stroke={annStroke}\n strokeWidth={isSelected ? 4 : isHovered ? 3 : 2}\n fill={annFill}\n style={{\n pointerEvents: 'auto',\n cursor: 'pointer',\n opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7\n }}\n onClick={() => eventBus?.emit('annotation:click', { annotationId: ann.id, motivation: ann.motivation })}\n onMouseEnter={() => handleMouseEnter(ann.id)}\n onMouseLeave={handleMouseLeave}\n />\n );\n })}\n\n {/* Render current selection while drawing or awaiting save */}\n {selection && (() => {\n const rectX = Math.min(selection.startX, selection.endX);\n const rectY = Math.min(selection.startY, selection.endY);\n const rectWidth = Math.abs(selection.endX - selection.startX);\n const rectHeight = Math.abs(selection.endY - selection.startY);\n\n // PDF only supports rectangle shapes (FragmentSelector with viewrect)\n // Circle/polygon are disabled in the UI for PDF media types\n return (\n <rect\n x={rectX}\n y={rectY}\n width={rectWidth}\n height={rectHeight}\n stroke={stroke}\n strokeWidth={2}\n strokeDasharray=\"5,5\"\n fill={fill}\n pointerEvents=\"none\"\n />\n );\n })()}\n </svg>\n </div>\n </div>\n )}\n </div>\n\n {/* Page navigation controls */}\n {numPages > 0 && (\n <div className=\"semiont-pdf-annotation-canvas__controls\">\n <button\n disabled={pageNumber <= 1}\n onClick={() => setPageNumber(pageNumber - 1)}\n className=\"semiont-pdf-annotation-canvas__button\"\n >\n Previous\n </button>\n <span className=\"semiont-pdf-annotation-canvas__page-info\">\n Page {pageNumber} of {numPages}\n </span>\n <button\n disabled={pageNumber >= numPages}\n onClick={() => setPageNumber(pageNumber + 1)}\n className=\"semiont-pdf-annotation-canvas__button\"\n >\n Next\n </button>\n </div>\n )}\n </div>\n );\n}\n","/**\n * PDF Coordinate Utilities\n *\n * Handles coordinate transformations between:\n * - Canvas space (pixels, top-left origin, Y increases downward)\n * - PDF space (points, bottom-left origin, Y increases upward)\n *\n * Based on RFC 3778 PDF Fragment Identifiers:\n * https://tools.ietf.org/html/rfc3778\n */\n\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface PdfCoordinate {\n page: number;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface CanvasRectangle {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}\n\n/**\n * Convert canvas coordinates to PDF coordinates\n *\n * Canvas: Origin at top-left, Y increases downward\n * PDF: Origin at bottom-left, Y increases upward\n *\n * @param canvasRect - Rectangle in canvas pixel coordinates\n * @param page - PDF page number (1-indexed)\n * @param pageWidth - PDF page width in points\n * @param pageHeight - PDF page height in points\n * @param scale - Current canvas scale factor\n */\nexport function canvasToPdfCoordinates(\n canvasRect: CanvasRectangle,\n page: number,\n _pageWidth: number,\n pageHeight: number,\n scale: number = 1\n): PdfCoordinate {\n // Normalize rectangle (handle drag in any direction)\n const x1 = Math.min(canvasRect.startX, canvasRect.endX);\n const y1 = Math.min(canvasRect.startY, canvasRect.endY);\n const x2 = Math.max(canvasRect.startX, canvasRect.endX);\n const y2 = Math.max(canvasRect.startY, canvasRect.endY);\n\n // Convert from canvas pixels to PDF points\n const pdfX = x1 / scale;\n const pdfWidth = (x2 - x1) / scale;\n\n // Flip Y coordinate (canvas top-left to PDF bottom-left)\n const pdfY = pageHeight - (y2 / scale);\n const pdfHeight = (y2 - y1) / scale;\n\n return {\n page,\n x: Math.round(pdfX),\n y: Math.round(pdfY),\n width: Math.round(pdfWidth),\n height: Math.round(pdfHeight)\n };\n}\n\n/**\n * Convert PDF coordinates to canvas coordinates\n *\n * @param pdfCoord - Coordinates in PDF space\n * @param pageHeight - PDF page height in points\n * @param scale - Current canvas scale factor\n */\nexport function pdfToCanvasCoordinates(\n pdfCoord: PdfCoordinate,\n pageHeight: number,\n scale: number = 1\n): Rectangle {\n // Convert from PDF points to canvas pixels\n const canvasX = pdfCoord.x * scale;\n const canvasWidth = pdfCoord.width * scale;\n\n // Flip Y coordinate (PDF bottom-left to canvas top-left)\n const canvasY = (pageHeight - pdfCoord.y - pdfCoord.height) * scale;\n const canvasHeight = pdfCoord.height * scale;\n\n return {\n x: canvasX,\n y: canvasY,\n width: canvasWidth,\n height: canvasHeight\n };\n}\n\n/**\n * Generate RFC 3778 FragmentSelector value\n *\n * Format: page=N&viewrect=left,top,width,height\n * All coordinates in PDF points\n */\nexport function createFragmentSelector(coord: PdfCoordinate): string {\n return `page=${coord.page}&viewrect=${coord.x},${coord.y},${coord.width},${coord.height}`;\n}\n\n/**\n * Parse RFC 3778 FragmentSelector value\n *\n * @param fragment - Fragment string like \"page=5&viewrect=100,200,300,400\"\n * @returns Parsed PDF coordinates or null if invalid\n */\nexport function parseFragmentSelector(fragment: string): PdfCoordinate | null {\n try {\n // Parse page number\n const pageMatch = fragment.match(/page=(\\d+)/);\n if (!pageMatch) return null;\n const page = parseInt(pageMatch[1], 10);\n\n // Parse viewrect coordinates\n const viewrectMatch = fragment.match(/viewrect=([\\d.]+),([\\d.]+),([\\d.]+),([\\d.]+)/);\n if (!viewrectMatch) return null;\n\n return {\n page,\n x: parseFloat(viewrectMatch[1]),\n y: parseFloat(viewrectMatch[2]),\n width: parseFloat(viewrectMatch[3]),\n height: parseFloat(viewrectMatch[4])\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Extract page number from FragmentSelector\n */\nexport function getPageFromFragment(fragment: string): number | null {\n const match = fragment.match(/page=(\\d+)/);\n return match ? parseInt(match[1], 10) : null;\n}\n","/**\n * Browser PDF.js utilities\n *\n * Uses native browser PDF.js when available, falls back to CDN.\n * Zero npm dependencies - no webpack bundling issues.\n */\n\n// Type definitions for PDF.js API\nexport interface PDFDocumentProxy {\n numPages: number;\n getPage(pageNumber: number): Promise<PDFPageProxy>;\n}\n\nexport interface PDFPageProxy {\n getViewport(params: { scale: number; rotation?: number }): PDFViewport;\n render(params: PDFRenderParams): PDFRenderTask;\n getTextContent(): Promise<TextContent>;\n}\n\nexport interface PDFViewport {\n width: number;\n height: number;\n scale: number;\n rotation: number;\n}\n\nexport interface PDFRenderParams {\n canvasContext: CanvasRenderingContext2D;\n viewport: PDFViewport;\n}\n\nexport interface PDFRenderTask {\n promise: Promise<void>;\n cancel(): void;\n}\n\nexport interface PDFLib {\n getDocument(params: { data: ArrayBuffer } | { url: string }): PDFLoadingTask;\n GlobalWorkerOptions: {\n workerSrc: string;\n };\n version: string;\n}\n\nexport interface PDFLoadingTask {\n promise: Promise<PDFDocumentProxy>;\n destroy(): void;\n}\n\n/**\n * Text content types (for Phase 2)\n */\nexport interface TextItem {\n str: string;\n dir: string;\n transform: number[]; // [scaleX, skewX, skewY, scaleY, x, y]\n width: number;\n height: number;\n fontName: string;\n hasEOL: boolean;\n}\n\nexport interface TextContent {\n items: TextItem[];\n styles: Record<string, any>;\n}\n\n/**\n * Ensure PDF.js is available, loading from local public folder if needed\n */\nexport async function ensurePdfJs(): Promise<PDFLib> {\n // Check if already available (browser native or already loaded)\n if (typeof window !== 'undefined' && (window as any).pdfjsLib) {\n return (window as any).pdfjsLib as PDFLib;\n }\n\n // Load from local public folder (staged during build)\n return new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.src = '/pdfjs/pdf.min.mjs';\n script.type = 'module';\n\n script.onload = () => {\n const pdfjsLib = (window as any).pdfjsLib as PDFLib;\n\n if (!pdfjsLib) {\n reject(new Error('PDF.js loaded but pdfjsLib not available'));\n return;\n }\n\n // Configure worker (also served from local public folder)\n pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.min.mjs';\n\n resolve(pdfjsLib);\n };\n\n script.onerror = () => {\n reject(new Error('Failed to load PDF.js from /pdfjs/pdf.min.mjs'));\n };\n\n document.head.appendChild(script);\n });\n}\n\n/**\n * Load PDF document from URL or ArrayBuffer\n *\n * When given a URL string, fetches the PDF as ArrayBuffer with credentials\n * to ensure authentication cookies are included in the request.\n */\nexport async function loadPdfDocument(\n source: string | ArrayBuffer\n): Promise<PDFDocumentProxy> {\n const pdfjsLib = await ensurePdfJs();\n\n if (typeof source === 'string') {\n // Fetch as ArrayBuffer first to include authentication cookies\n const response = await fetch(source, {\n credentials: 'include',\n headers: {\n 'Accept': 'application/pdf',\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });\n return loadingTask.promise;\n } else {\n const loadingTask = pdfjsLib.getDocument({ data: source });\n return loadingTask.promise;\n }\n}\n\n/**\n * Render PDF page to canvas and return as data URL\n */\nexport async function renderPdfPageToDataUrl(\n page: PDFPageProxy,\n scale = 1.0\n): Promise<{ dataUrl: string; width: number; height: number }> {\n const viewport = page.getViewport({ scale });\n\n // Create canvas\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) {\n throw new Error('Failed to get 2D context');\n }\n\n canvas.width = viewport.width;\n canvas.height = viewport.height;\n\n // Render PDF page to canvas\n const renderTask = page.render({\n canvasContext: context,\n viewport: viewport\n });\n\n await renderTask.promise;\n\n // Convert to data URL\n return {\n dataUrl: canvas.toDataURL('image/png'),\n width: viewport.width,\n height: viewport.height\n };\n}\n\n/**\n * Render PDF page with text content extraction (Phase 2)\n *\n * This function extracts text in parallel with rendering for future\n * text layer support. Currently the text content is available but not used.\n */\nexport async function renderPdfPageWithText(\n page: PDFPageProxy,\n scale = 1.0\n): Promise<{\n dataUrl: string;\n width: number;\n height: number;\n textContent: TextContent;\n}> {\n const viewport = page.getViewport({ scale });\n\n // Create canvas for rendering\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) {\n throw new Error('Failed to get 2D context');\n }\n\n canvas.width = viewport.width;\n canvas.height = viewport.height;\n\n // Render PDF page to canvas\n const renderTask = page.render({\n canvasContext: context,\n viewport: viewport\n });\n\n // Extract text content in parallel (for future text layer support)\n const [, textContent] = await Promise.all([\n renderTask.promise,\n page.getTextContent()\n ]);\n\n // Convert to data URL\n return {\n dataUrl: canvas.toDataURL('image/png'),\n width: viewport.width,\n height: viewport.height,\n textContent\n };\n}\n"],"mappings":"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","names":["error","pdfCoord","scaleX","scaleY"]}
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+ import {
3
+ useEventBus
4
+ } from "./chunk-XS27QKGP.mjs";
5
+
6
+ // src/contexts/useEventSubscription.ts
7
+ import { useEffect, useRef, useMemo } from "react";
8
+ function useEventSubscription(eventName, handler) {
9
+ const eventBus = useEventBus();
10
+ const handlerRef = useRef(handler);
11
+ useEffect(() => {
12
+ handlerRef.current = handler;
13
+ });
14
+ useEffect(() => {
15
+ const stableHandler = (payload) => {
16
+ handlerRef.current(payload);
17
+ };
18
+ eventBus.on(eventName, stableHandler);
19
+ return () => {
20
+ eventBus.off(eventName, stableHandler);
21
+ };
22
+ }, [eventName]);
23
+ }
24
+ function useEventSubscriptions(subscriptions) {
25
+ const eventBus = useEventBus();
26
+ const handlersRef = useRef(subscriptions);
27
+ useEffect(() => {
28
+ handlersRef.current = subscriptions;
29
+ });
30
+ const eventNames = useMemo(
31
+ () => Object.keys(subscriptions).sort(),
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ [Object.keys(subscriptions).sort().join(",")]
34
+ );
35
+ useEffect(() => {
36
+ const stableHandlers = /* @__PURE__ */ new Map();
37
+ for (const eventName of eventNames) {
38
+ const stableHandler = (payload) => {
39
+ const currentHandler = handlersRef.current[eventName];
40
+ if (currentHandler) {
41
+ currentHandler(payload);
42
+ } else {
43
+ console.warn("[useEventSubscriptions] No current handler found for:", eventName);
44
+ }
45
+ };
46
+ stableHandlers.set(eventName, stableHandler);
47
+ eventBus.on(eventName, stableHandler);
48
+ }
49
+ return () => {
50
+ for (const [eventName, stableHandler] of stableHandlers) {
51
+ eventBus.off(eventName, stableHandler);
52
+ }
53
+ };
54
+ }, [eventNames]);
55
+ }
56
+
57
+ // src/hooks/useAttentionFlow.ts
58
+ import { useState, useRef as useRef2, useCallback } from "react";
59
+ function useAttentionFlow() {
60
+ const eventBus = useEventBus();
61
+ const [hoveredAnnotationId, setHoveredAnnotationId] = useState(null);
62
+ const handleAnnotationHover = useCallback(({ annotationId }) => {
63
+ setHoveredAnnotationId(annotationId);
64
+ if (annotationId) {
65
+ eventBus.emit("annotation:sparkle", { annotationId });
66
+ }
67
+ }, []);
68
+ const handleAnnotationClick = useCallback(({ annotationId }) => {
69
+ eventBus.emit("annotation:focus", { annotationId });
70
+ }, []);
71
+ useEventSubscriptions({
72
+ "annotation:hover": handleAnnotationHover,
73
+ "annotation:click": handleAnnotationClick
74
+ });
75
+ return { hoveredAnnotationId };
76
+ }
77
+ var HOVER_DELAY_MS = 150;
78
+ function createHoverHandlers(emit) {
79
+ let currentHover = null;
80
+ let timer = null;
81
+ const cancelTimer = () => {
82
+ if (timer !== null) {
83
+ clearTimeout(timer);
84
+ timer = null;
85
+ }
86
+ };
87
+ const handleMouseEnter = (annotationId) => {
88
+ if (currentHover === annotationId) return;
89
+ cancelTimer();
90
+ timer = setTimeout(() => {
91
+ timer = null;
92
+ currentHover = annotationId;
93
+ emit(annotationId);
94
+ }, HOVER_DELAY_MS);
95
+ };
96
+ const handleMouseLeave = () => {
97
+ cancelTimer();
98
+ if (currentHover !== null) {
99
+ currentHover = null;
100
+ emit(null);
101
+ }
102
+ };
103
+ return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
104
+ }
105
+ function useHoverEmitter(annotationId) {
106
+ const eventBus = useEventBus();
107
+ const currentHoverRef = useRef2(null);
108
+ const timerRef = useRef2(null);
109
+ const onMouseEnter = useCallback(() => {
110
+ if (currentHoverRef.current === annotationId) return;
111
+ if (timerRef.current !== null) {
112
+ clearTimeout(timerRef.current);
113
+ }
114
+ timerRef.current = setTimeout(() => {
115
+ timerRef.current = null;
116
+ currentHoverRef.current = annotationId;
117
+ eventBus.emit("annotation:hover", { annotationId });
118
+ }, HOVER_DELAY_MS);
119
+ }, [annotationId]);
120
+ const onMouseLeave = useCallback(() => {
121
+ if (timerRef.current !== null) {
122
+ clearTimeout(timerRef.current);
123
+ timerRef.current = null;
124
+ }
125
+ if (currentHoverRef.current !== null) {
126
+ currentHoverRef.current = null;
127
+ eventBus.emit("annotation:hover", { annotationId: null });
128
+ }
129
+ }, []);
130
+ return { onMouseEnter, onMouseLeave };
131
+ }
132
+
133
+ export {
134
+ useEventSubscription,
135
+ useEventSubscriptions,
136
+ useAttentionFlow,
137
+ HOVER_DELAY_MS,
138
+ createHoverHandlers,
139
+ useHoverEmitter
140
+ };
141
+ //# sourceMappingURL=chunk-FC6SGLLT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contexts/useEventSubscription.ts","../src/hooks/useAttentionFlow.ts"],"sourcesContent":["import { useEffect, useRef, useMemo } from 'react';\nimport type { EventMap } from './EventBusContext';\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('annotation: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 eventBus.on(eventName, stableHandler);\n\n return () => {\n eventBus.off(eventName, stableHandler);\n };\n }, [eventName]); // 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 * 'annotation:created': ({ annotation }) => setNewAnnotation(annotation),\n * 'annotation: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 stableHandlers = new Map<keyof EventMap, (payload: any) => 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 stableHandlers.set(eventName as keyof EventMap, stableHandler);\n eventBus.on(eventName as keyof EventMap, stableHandler);\n }\n\n // Cleanup all subscriptions\n return () => {\n for (const [eventName, stableHandler] of stableHandlers) {\n eventBus.off(eventName, stableHandler);\n }\n };\n }, [eventNames]); // eventBus is stable singleton - never in deps; only re-subscribe if event names change\n}\n","/**\n * useAttentionFlow — 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: annotation:sparkle visual effect (triggerSparkleAnimation) is owned by\n * ResourceViewerPage, which subscribes to annotation:sparkle and delegates to\n * ResourceAnnotationsContext. This hook emits the signal; it does not render the effect.\n *\n * @subscribes annotation:hover - Sets hoveredAnnotationId; emits annotation:sparkle\n * @subscribes annotation:click - Emits annotation:focus (attention relay only)\n * @emits annotation:sparkle\n * @emits annotation: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 * annotation: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 } from 'react';\nimport { useEventBus } from '../contexts/EventBusContext';\nimport { useEventSubscriptions } from '../contexts/useEventSubscription';\n\n// ─── useAttentionFlow ─────────────────────────────────────────────────────────\n\nexport interface AttentionFlowState {\n hoveredAnnotationId: string | null;\n}\n\nexport function useAttentionFlow(): AttentionFlowState {\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.emit('annotation:sparkle', { annotationId });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n const handleAnnotationClick = useCallback(({ annotationId }: { annotationId: string }) => {\n eventBus.emit('annotation:focus', { annotationId });\n // Scroll to annotation handled by BrowseView via annotation:focus subscription\n }, []); // eventBus is stable singleton - never in deps\n\n useEventSubscriptions({\n 'annotation:hover': handleAnnotationHover,\n 'annotation:click': handleAnnotationClick,\n });\n\n return { hoveredAnnotationId };\n}\n\n// ─── createHoverHandlers (use inside useEffect / imperative setup) ────────────\n\n/** Milliseconds the mouse must dwell before annotation: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\nexport function createHoverHandlers(emit: EmitHover): 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 }, HOVER_DELAY_MS);\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 */\nexport function useHoverEmitter(annotationId: string): 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.emit('annotation:hover', { annotationId });\n }, HOVER_DELAY_MS);\n }, [annotationId]); // 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.emit('annotation:hover', { annotationId: null });\n }\n }, []); // eventBus is stable singleton - never in deps\n\n return { onMouseEnter, onMouseLeave };\n}\n"],"mappings":"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","names":["useRef","useRef"]}
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ // src/contexts/EventBusContext.tsx
4
+ import { createContext, useContext, useMemo } from "react";
5
+ import mitt from "mitt";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var EventBusContext = createContext(null);
8
+ function generateBusId() {
9
+ return Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
10
+ }
11
+ function createEventBus() {
12
+ const bus = mitt();
13
+ const busId = generateBusId();
14
+ bus.busId = busId;
15
+ const originalEmit = bus.emit.bind(bus);
16
+ bus.emit = (eventName, payload) => {
17
+ console.info(`[EventBus:${busId}] emit:`, eventName, payload);
18
+ return originalEmit(eventName, payload);
19
+ };
20
+ const originalOn = bus.on.bind(bus);
21
+ bus.on = (eventName, handler) => {
22
+ console.debug(`[EventBus:${busId}] subscribe:`, eventName);
23
+ return originalOn(eventName, handler);
24
+ };
25
+ const originalOff = bus.off.bind(bus);
26
+ bus.off = (eventName, handler) => {
27
+ console.debug(`[EventBus:${busId}] unsubscribe:`, eventName);
28
+ return originalOff(eventName, handler);
29
+ };
30
+ return bus;
31
+ }
32
+ var globalEventBus = createEventBus();
33
+ function resetEventBusForTesting() {
34
+ globalEventBus = createEventBus();
35
+ }
36
+ function EventBusProvider({
37
+ children
38
+ }) {
39
+ const eventBus = useMemo(() => globalEventBus, []);
40
+ return /* @__PURE__ */ jsx(EventBusContext.Provider, { value: eventBus, children });
41
+ }
42
+ function useEventBus() {
43
+ const bus = useContext(EventBusContext);
44
+ if (!bus) {
45
+ throw new Error("useEventBus must be used within EventBusProvider");
46
+ }
47
+ return bus;
48
+ }
49
+
50
+ export {
51
+ resetEventBusForTesting,
52
+ EventBusProvider,
53
+ useEventBus
54
+ };
55
+ //# sourceMappingURL=chunk-XS27QKGP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contexts/EventBusContext.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport mitt from 'mitt';\nimport type { Handler } from 'mitt';\nimport type { ResourceEvent } from '@semiont/core';\nimport type { components, Selector, ResourceUri, GenerationContext } from '@semiont/api-client';\nimport type { DetectionProgress, GenerationProgress } from '../types/progress';\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 (backend real-time stream via GET /resources/:id/events/stream)\n 'detection:started': Extract<ResourceEvent, { type: 'job.started' }>;\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 // Detection progress from SSE detection streams (all 5 motivation types)\n 'detection:progress': DetectionProgress;\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: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; cancelFallback: () => void };\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: components['schemas']['AnnotationBody'][];\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?: components['schemas']['AnnotationBody'];\n oldItem?: components['schemas']['AnnotationBody'];\n newItem?: components['schemas']['AnnotationBody'];\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 /** Comment tone */\n tone?: 'scholarly' | 'explanatory' | 'conversational' | 'technical' | 'analytical' | 'critical' | 'balanced' | 'constructive';\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?: DetectionProgress };\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: GenerationContext;\n };\n };\n 'generation:progress': GenerationProgress;\n 'generation:complete': { annotationUri: string; progress: GenerationProgress };\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 'resolution:search-requested': {\n referenceId: string;\n searchTerm: string;\n };\n 'context:retrieval-requested': {\n annotationUri: string;\n resourceUri: string;\n };\n 'context:retrieval-complete': {\n annotationUri: string;\n context: GenerationContext;\n };\n 'context:retrieval-failed': {\n annotationUri: string;\n error: Error;\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 = <Key extends keyof EventMap>(eventName: Key, payload?: EventMap[Key]) => {\n console.info(`[EventBus:${busId}] emit:`, eventName, payload);\n return originalEmit(eventName, payload as EventMap[Key]);\n };\n\n // Wrap on to add logging with busId\n const originalOn = bus.on.bind(bus);\n bus.on = <Key extends keyof EventMap>(eventName: Key, handler: Handler<EventMap[Key]>) => {\n console.debug(`[EventBus:${busId}] subscribe:`, eventName);\n return originalOn(eventName, handler);\n };\n\n // Wrap off to add logging with busId\n const originalOff = bus.off.bind(bus);\n bus.off = <Key extends keyof EventMap>(eventName: Key, handler?: Handler<EventMap[Key]>) => {\n console.debug(`[EventBus:${busId}] unsubscribe:`, eventName);\n return originalOff(eventName, handler);\n };\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 useResolutionFlow 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 useResolutionFlow 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"],"mappings":";;;AAEA,SAAS,eAAe,YAAY,eAA+B;AACnE,OAAO,UAAU;AAyUb;AA3HJ,IAAM,kBAAkB,cAA+B,IAAI;AAK3D,SAAS,gBAAwB;AAC/B,SAAO,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC5E;AAKA,SAAS,iBAA2B;AAClC,QAAM,MAAM,KAAe;AAC3B,QAAM,QAAQ,cAAc;AAG5B,MAAI,QAAQ;AAGZ,QAAM,eAAe,IAAI,KAAK,KAAK,GAAG;AACtC,MAAI,OAAO,CAA6B,WAAgB,YAA4B;AAClF,YAAQ,KAAK,aAAa,KAAK,WAAW,WAAW,OAAO;AAC5D,WAAO,aAAa,WAAW,OAAwB;AAAA,EACzD;AAGA,QAAM,aAAa,IAAI,GAAG,KAAK,GAAG;AAClC,MAAI,KAAK,CAA6B,WAAgB,YAAoC;AACxF,YAAQ,MAAM,aAAa,KAAK,gBAAgB,SAAS;AACzD,WAAO,WAAW,WAAW,OAAO;AAAA,EACtC;AAGA,QAAM,cAAc,IAAI,IAAI,KAAK,GAAG;AACpC,MAAI,MAAM,CAA6B,WAAgB,YAAqC;AAC1F,YAAQ,MAAM,aAAa,KAAK,kBAAkB,SAAS;AAC3D,WAAO,YAAY,WAAW,OAAO;AAAA,EACvC;AAEA,SAAO;AACT;AA+BA,IAAI,iBAAiB,eAAe;AAe7B,SAAS,0BAA0B;AACxC,mBAAiB,eAAe;AAClC;AA4BO,SAAS,iBAAiB;AAAA,EAC/B;AACF,GAA0B;AACxB,QAAM,WAAW,QAAQ,MAAM,gBAAgB,CAAC,CAAC;AAEjD,SACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,UAC9B,UACH;AAEJ;AA2BO,SAAS,cAAwB;AACtC,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO;AACT;","names":[]}
@@ -34,65 +34,18 @@ function useApiClient() {
34
34
  return context;
35
35
  }
36
36
 
37
- // src/contexts/EventBusContext.tsx
38
- import { createContext as createContext2, useContext as useContext2, useMemo as useMemo2 } from "react";
39
- import mitt from "mitt";
40
- import { jsx as jsx2 } from "react/jsx-runtime";
41
- var EventBusContext = createContext2(null);
42
- function generateBusId() {
43
- return Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
44
- }
45
- function createEventBus() {
46
- const bus = mitt();
47
- const busId = generateBusId();
48
- bus.busId = busId;
49
- const originalEmit = bus.emit.bind(bus);
50
- bus.emit = (eventName, payload) => {
51
- console.info(`[EventBus:${busId}] emit:`, eventName, payload);
52
- return originalEmit(eventName, payload);
53
- };
54
- const originalOn = bus.on.bind(bus);
55
- bus.on = (eventName, handler) => {
56
- console.debug(`[EventBus:${busId}] subscribe:`, eventName);
57
- return originalOn(eventName, handler);
58
- };
59
- const originalOff = bus.off.bind(bus);
60
- bus.off = (eventName, handler) => {
61
- console.debug(`[EventBus:${busId}] unsubscribe:`, eventName);
62
- return originalOff(eventName, handler);
63
- };
64
- return bus;
65
- }
66
- var globalEventBus = createEventBus();
67
- function resetEventBusForTesting() {
68
- globalEventBus = createEventBus();
69
- }
70
- function EventBusProvider({
71
- children
72
- }) {
73
- const eventBus = useMemo2(() => globalEventBus, []);
74
- return /* @__PURE__ */ jsx2(EventBusContext.Provider, { value: eventBus, children });
75
- }
76
- function useEventBus() {
77
- const bus = useContext2(EventBusContext);
78
- if (!bus) {
79
- throw new Error("useEventBus must be used within EventBusProvider");
80
- }
81
- return bus;
82
- }
83
-
84
37
  // src/contexts/SessionContext.tsx
85
- import { createContext as createContext3, useContext as useContext3 } from "react";
86
- import { jsx as jsx3 } from "react/jsx-runtime";
87
- var SessionContext = createContext3(null);
38
+ import { createContext as createContext2, useContext as useContext2 } from "react";
39
+ import { jsx as jsx2 } from "react/jsx-runtime";
40
+ var SessionContext = createContext2(null);
88
41
  function SessionProvider({
89
42
  sessionManager,
90
43
  children
91
44
  }) {
92
- return /* @__PURE__ */ jsx3(SessionContext.Provider, { value: sessionManager, children });
45
+ return /* @__PURE__ */ jsx2(SessionContext.Provider, { value: sessionManager, children });
93
46
  }
94
47
  function useSessionContext() {
95
- const context = useContext3(SessionContext);
48
+ const context = useContext2(SessionContext);
96
49
  if (!context) {
97
50
  throw new Error("useSessionContext must be used within SessionProvider");
98
51
  }
@@ -102,12 +55,12 @@ function useSessionContext() {
102
55
  // src/components/Toast.tsx
103
56
  import React, { useEffect, useState } from "react";
104
57
  import { createPortal } from "react-dom";
105
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
58
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
106
59
  var icons = {
107
- success: /* @__PURE__ */ jsx4("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }),
108
- error: /* @__PURE__ */ jsx4("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }),
109
- warning: /* @__PURE__ */ jsx4("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("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" }) }),
110
- info: /* @__PURE__ */ jsx4("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("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" }) })
60
+ success: /* @__PURE__ */ jsx3("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }),
61
+ error: /* @__PURE__ */ jsx3("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }),
62
+ warning: /* @__PURE__ */ jsx3("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("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" }) }),
63
+ info: /* @__PURE__ */ jsx3("svg", { className: "semiont-toast-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("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" }) })
111
64
  };
112
65
  function Toast({ toast, onClose }) {
113
66
  useEffect(() => {
@@ -123,15 +76,15 @@ function Toast({ toast, onClose }) {
123
76
  "data-variant": toast.type,
124
77
  role: "alert",
125
78
  children: [
126
- /* @__PURE__ */ jsx4("div", { className: "semiont-toast-icon-wrapper", children: icons[toast.type] }),
127
- /* @__PURE__ */ jsx4("p", { className: "semiont-toast-message", children: toast.message }),
128
- /* @__PURE__ */ jsx4(
79
+ /* @__PURE__ */ jsx3("div", { className: "semiont-toast-icon-wrapper", children: icons[toast.type] }),
80
+ /* @__PURE__ */ jsx3("p", { className: "semiont-toast-message", children: toast.message }),
81
+ /* @__PURE__ */ jsx3(
129
82
  "button",
130
83
  {
131
84
  onClick: () => onClose(toast.id),
132
85
  className: "semiont-toast-close",
133
86
  "aria-label": "Close",
134
- children: /* @__PURE__ */ jsx4("svg", { className: "semiont-toast-close-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
87
+ children: /* @__PURE__ */ jsx3("svg", { className: "semiont-toast-close-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
135
88
  }
136
89
  )
137
90
  ]
@@ -146,7 +99,7 @@ function ToastContainer({ toasts, onClose }) {
146
99
  }, []);
147
100
  if (!mounted) return null;
148
101
  return createPortal(
149
- /* @__PURE__ */ jsx4("div", { className: "semiont-toast-container", children: toasts.map((toast) => /* @__PURE__ */ jsx4(Toast, { toast, onClose }, toast.id)) }),
102
+ /* @__PURE__ */ jsx3("div", { className: "semiont-toast-container", children: toasts.map((toast) => /* @__PURE__ */ jsx3(Toast, { toast, onClose }, toast.id)) }),
150
103
  document.body
151
104
  );
152
105
  }
@@ -171,7 +124,7 @@ function ToastProvider({ children }) {
171
124
  );
172
125
  return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: contextValue, children: [
173
126
  children,
174
- /* @__PURE__ */ jsx4(ToastContainer, { toasts, onClose: handleClose })
127
+ /* @__PURE__ */ jsx3(ToastContainer, { toasts, onClose: handleClose })
175
128
  ] });
176
129
  }
177
130
  function useToast() {
@@ -183,17 +136,17 @@ function useToast() {
183
136
  }
184
137
 
185
138
  // src/contexts/OpenResourcesContext.tsx
186
- import { createContext as createContext4, useContext as useContext4 } from "react";
187
- import { jsx as jsx5 } from "react/jsx-runtime";
188
- var OpenResourcesContext = createContext4(void 0);
139
+ import { createContext as createContext3, useContext as useContext3 } from "react";
140
+ import { jsx as jsx4 } from "react/jsx-runtime";
141
+ var OpenResourcesContext = createContext3(void 0);
189
142
  function OpenResourcesProvider({
190
143
  openResourcesManager,
191
144
  children
192
145
  }) {
193
- return /* @__PURE__ */ jsx5(OpenResourcesContext.Provider, { value: openResourcesManager, children });
146
+ return /* @__PURE__ */ jsx4(OpenResourcesContext.Provider, { value: openResourcesManager, children });
194
147
  }
195
148
  function useOpenResources() {
196
- const context = useContext4(OpenResourcesContext);
149
+ const context = useContext3(OpenResourcesContext);
197
150
  if (context === void 0) {
198
151
  throw new Error("useOpenResources must be used within an OpenResourcesProvider");
199
152
  }
@@ -201,8 +154,8 @@ function useOpenResources() {
201
154
  }
202
155
 
203
156
  // src/contexts/TranslationContext.tsx
204
- import { createContext as createContext5, useContext as useContext5, useState as useState2, useEffect as useEffect2, useMemo as useMemo3 } from "react";
205
- import { Fragment, jsx as jsx6 } from "react/jsx-runtime";
157
+ import { createContext as createContext4, useContext as useContext4, useState as useState2, useEffect as useEffect2, useMemo as useMemo2 } from "react";
158
+ import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
206
159
 
207
160
  // import("../../translations/**/*.json") in src/contexts/TranslationContext.tsx
208
161
  var globImport_translations_json = __glob({
@@ -238,7 +191,7 @@ var globImport_translations_json = __glob({
238
191
  });
239
192
 
240
193
  // src/contexts/TranslationContext.tsx
241
- var TranslationContext = createContext5(null);
194
+ var TranslationContext = createContext4(null);
242
195
  var translationCache = /* @__PURE__ */ new Map();
243
196
  function processPluralFormat(text, params) {
244
197
  const pluralMatch = text.match(/\{(\w+),\s*plural,\s*/);
@@ -402,7 +355,7 @@ function TranslationProvider({
402
355
  });
403
356
  }
404
357
  }, [locale, translationManager]);
405
- const localeManager = useMemo3(() => {
358
+ const localeManager = useMemo2(() => {
406
359
  if (!loadedTranslations) return null;
407
360
  return {
408
361
  t: (namespace, key, params) => {
@@ -424,18 +377,18 @@ function TranslationProvider({
424
377
  };
425
378
  }, [loadedTranslations, locale]);
426
379
  if (translationManager) {
427
- return /* @__PURE__ */ jsx6(TranslationContext.Provider, { value: translationManager, children });
380
+ return /* @__PURE__ */ jsx5(TranslationContext.Provider, { value: translationManager, children });
428
381
  }
429
382
  if (locale && isLoading) {
430
- return /* @__PURE__ */ jsx6(Fragment, { children: loadingComponent });
383
+ return /* @__PURE__ */ jsx5(Fragment, { children: loadingComponent });
431
384
  }
432
385
  if (locale && localeManager) {
433
- return /* @__PURE__ */ jsx6(TranslationContext.Provider, { value: localeManager, children });
386
+ return /* @__PURE__ */ jsx5(TranslationContext.Provider, { value: localeManager, children });
434
387
  }
435
- return /* @__PURE__ */ jsx6(TranslationContext.Provider, { value: defaultTranslationManager, children });
388
+ return /* @__PURE__ */ jsx5(TranslationContext.Provider, { value: defaultTranslationManager, children });
436
389
  }
437
390
  function useTranslations(namespace) {
438
- const context = useContext5(TranslationContext);
391
+ const context = useContext4(TranslationContext);
439
392
  if (!context) {
440
393
  return (key, params) => {
441
394
  const translations = en_default;
@@ -475,9 +428,6 @@ function usePreloadTranslations() {
475
428
  export {
476
429
  ApiClientProvider,
477
430
  useApiClient,
478
- resetEventBusForTesting,
479
- EventBusProvider,
480
- useEventBus,
481
431
  SessionProvider,
482
432
  useSessionContext,
483
433
  ToastContainer,
@@ -490,4 +440,4 @@ export {
490
440
  useTranslations,
491
441
  usePreloadTranslations
492
442
  };
493
- //# sourceMappingURL=chunk-QB52Q7EQ.mjs.map
443
+ //# sourceMappingURL=chunk-YPYLOBA2.mjs.map