@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
package/README.md CHANGED
@@ -74,26 +74,32 @@ Import the styles in your app's main CSS file:
74
74
  import {
75
75
  TranslationProvider,
76
76
  ApiClientProvider,
77
- SessionProvider,
77
+ KnowledgeBaseSessionProvider,
78
+ ProtectedErrorBoundary,
79
+ SessionExpiredModal,
80
+ PermissionDeniedModal,
78
81
  } from '@semiont/react-ui';
79
82
  import { QueryClientProvider } from '@tanstack/react-query';
80
83
 
81
84
  function App({ children }) {
82
85
  const translationManager = useTranslationManager(); // Your implementation
83
- const apiClientManager = useApiClientManager(); // Your implementation
84
- const sessionManager = useSessionManager(); // Your implementation
85
86
  const queryClient = new QueryClient();
86
87
 
87
88
  return (
88
- <SessionProvider sessionManager={sessionManager}>
89
- <TranslationProvider translationManager={translationManager}>
90
- <ApiClientProvider apiClientManager={apiClientManager}>
91
- <QueryClientProvider client={queryClient}>
92
- {children}
93
- </QueryClientProvider>
89
+ <TranslationProvider translationManager={translationManager}>
90
+ <QueryClientProvider client={queryClient}>
91
+ <ApiClientProvider baseUrl="https://api.example.com">
92
+ {/* Mount KnowledgeBaseSessionProvider only on protected routes */}
93
+ <KnowledgeBaseSessionProvider>
94
+ <ProtectedErrorBoundary>
95
+ <SessionExpiredModal />
96
+ <PermissionDeniedModal />
97
+ {children}
98
+ </ProtectedErrorBoundary>
99
+ </KnowledgeBaseSessionProvider>
94
100
  </ApiClientProvider>
95
- </TranslationProvider>
96
- </SessionProvider>
101
+ </QueryClientProvider>
102
+ </TranslationProvider>
97
103
  );
98
104
  }
99
105
  ```
@@ -187,7 +193,7 @@ See [docs/STYLES.md](docs/STYLES.md) for detailed CSS documentation.
187
193
 
188
194
  All cross-cutting concerns use the Provider Pattern:
189
195
 
190
- - **SessionProvider** - Authentication state management
196
+ - **KnowledgeBaseSessionProvider** - KB list, active KB, validated session, modal state — one merged provider that's the single source of truth for "which KB and what's the session against it"
191
197
  - **TranslationProvider** - Internationalization
192
198
  - **ApiClientProvider** - Authenticated API client
193
199
  - **OpenResourcesProvider** - Recently opened resources
@@ -0,0 +1,174 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React__default from 'react';
3
+ import { components } from '@semiont/core';
4
+
5
+ /**
6
+ * Open Resources Manager Interface
7
+ *
8
+ * Manages a list of open resources (documents/files) with persistence.
9
+ * This interface allows apps to provide their own implementation of resource management
10
+ * (localStorage, sessionStorage, database, etc.) while components remain framework-agnostic.
11
+ *
12
+ * Components accept this manager as a prop instead of consuming from Context.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // In app (e.g., frontend/src/hooks/useOpenResourcesManager.ts)
17
+ * export function useOpenResourcesManager(): OpenResourcesManager {
18
+ * const [openResources, setOpenResources] = useState<OpenResource[]>([]);
19
+ *
20
+ * // Implementation details...
21
+ *
22
+ * return {
23
+ * openResources,
24
+ * addResource,
25
+ * removeResource,
26
+ * updateResourceName,
27
+ * reorderResources
28
+ * };
29
+ * }
30
+ *
31
+ * // Pass to components as props
32
+ * <KnowledgeNavigation openResourcesManager={openResourcesManager} />
33
+ * ```
34
+ */
35
+ interface OpenResource {
36
+ /** Unique identifier for the resource */
37
+ id: string;
38
+ /** Display name of the resource */
39
+ name: string;
40
+ /** Timestamp when the resource was opened */
41
+ openedAt: number;
42
+ /** Order/position for manual sorting (optional for backward compatibility) */
43
+ order?: number;
44
+ /** Media type for icon display (e.g., 'application/pdf', 'text/plain') */
45
+ mediaType?: string;
46
+ /** Working-tree URI (e.g. "file://docs/overview.md") — used as tooltip in navigation */
47
+ storageUri?: string;
48
+ }
49
+ interface OpenResourcesManager {
50
+ /** List of currently open resources */
51
+ openResources: OpenResource[];
52
+ /**
53
+ * Add a new resource to the open list or update if already exists
54
+ * @param id - Unique resource identifier
55
+ * @param name - Display name of the resource
56
+ * @param mediaType - Optional media type for icon display
57
+ * @param storageUri - Optional working-tree URI (e.g. "file://docs/overview.md")
58
+ */
59
+ addResource: (id: string, name: string, mediaType?: string, storageUri?: string) => void;
60
+ /**
61
+ * Remove a resource from the open list
62
+ * @param id - Resource identifier to remove
63
+ */
64
+ removeResource: (id: string) => void;
65
+ /**
66
+ * Update the display name of an open resource
67
+ * @param id - Resource identifier
68
+ * @param name - New display name
69
+ */
70
+ updateResourceName: (id: string, name: string) => void;
71
+ /**
72
+ * Reorder resources by moving from one index to another
73
+ * @param oldIndex - Current position index
74
+ * @param newIndex - Desired position index
75
+ */
76
+ reorderResources: (oldIndex: number, newIndex: number) => void;
77
+ }
78
+
79
+ /**
80
+ * KnowledgeBase — a connection to a Semiont backend instance.
81
+ *
82
+ * Each KB has its own JWT, its own API base URL, and its own session.
83
+ * The user is "authenticated against KB X" — never globally authenticated.
84
+ */
85
+ interface KnowledgeBase {
86
+ id: string;
87
+ label: string;
88
+ host: string;
89
+ port: number;
90
+ protocol: 'http' | 'https';
91
+ email: string;
92
+ }
93
+ /**
94
+ * Input shape for adding a new KB. The id is generated by the provider.
95
+ */
96
+ type NewKnowledgeBase = Omit<KnowledgeBase, 'id'>;
97
+ /**
98
+ * Status of the locally-stored credential for a KB. Derived from the
99
+ * presence and validity of the JWT in localStorage.
100
+ */
101
+ type KbSessionStatus = 'authenticated' | 'expired' | 'signed-out' | 'unreachable';
102
+
103
+ /**
104
+ * Translation management interface
105
+ * Apps implement this to provide translations using their preferred i18n library
106
+ */
107
+ interface TranslationManager {
108
+ /**
109
+ * Translate a key within a namespace
110
+ * @param namespace - Translation namespace (e.g., 'Toolbar', 'ResourceViewer')
111
+ * @param key - Translation key within the namespace
112
+ * @param params - Optional parameters for interpolation
113
+ * @returns Translated string
114
+ */
115
+ t: (namespace: string, key: string, params?: Record<string, any>) => string;
116
+ }
117
+
118
+ type UserInfo = components['schemas']['UserResponse'];
119
+ interface AuthSession {
120
+ token: string;
121
+ user: UserInfo;
122
+ }
123
+
124
+ interface KnowledgeBaseSessionValue {
125
+ knowledgeBases: KnowledgeBase[];
126
+ activeKnowledgeBase: KnowledgeBase | null;
127
+ session: AuthSession | null;
128
+ isLoading: boolean;
129
+ user: UserInfo | null;
130
+ token: string | null;
131
+ isAuthenticated: boolean;
132
+ hasValidBackendToken: boolean;
133
+ isFullyAuthenticated: boolean;
134
+ displayName: string;
135
+ avatarUrl: string | null;
136
+ userDomain: string | undefined;
137
+ isAdmin: boolean;
138
+ isModerator: boolean;
139
+ expiresAt: Date | null;
140
+ sessionExpiredAt: number | null;
141
+ sessionExpiredMessage: string | null;
142
+ permissionDeniedAt: number | null;
143
+ permissionDeniedMessage: string | null;
144
+ addKnowledgeBase: (kb: NewKnowledgeBase, access: string, refresh: string) => KnowledgeBase;
145
+ removeKnowledgeBase: (id: string) => void;
146
+ setActiveKnowledgeBase: (id: string) => void;
147
+ updateKnowledgeBase: (id: string, updates: Partial<Pick<KnowledgeBase, 'label'>>) => void;
148
+ /** Re-auth on an existing KB: store the new tokens and refresh the session. */
149
+ signIn: (id: string, access: string, refresh: string) => void;
150
+ /** Sign out of a KB: clear its stored tokens. If it's the active KB, clear in-memory session too. */
151
+ signOut: (id: string) => void;
152
+ /**
153
+ * Refresh the active KB's access token. Returns the new access token, or
154
+ * null if no refresh token is available or the refresh failed. Concurrent
155
+ * calls deduplicate via an in-flight Promise per KB. Used by the api-client's
156
+ * 401-recovery hook and by the proactive refresh timer.
157
+ */
158
+ refreshActive: () => Promise<string | null>;
159
+ acknowledgeSessionExpired: () => void;
160
+ acknowledgePermissionDenied: () => void;
161
+ }
162
+ /**
163
+ * Raw context export. Exposed for test utilities that need to construct
164
+ * a mock provider without going through localStorage and JWT validation.
165
+ * Production code should always use {@link useKnowledgeBaseSession} instead.
166
+ */
167
+ declare const KnowledgeBaseSessionContext: React__default.Context<KnowledgeBaseSessionValue | undefined>;
168
+
169
+ declare function KnowledgeBaseSessionProvider({ children }: {
170
+ children: React__default.ReactNode;
171
+ }): react_jsx_runtime.JSX.Element;
172
+ declare function useKnowledgeBaseSession(): KnowledgeBaseSessionValue;
173
+
174
+ export { type AuthSession as A, type KbSessionStatus as K, type NewKnowledgeBase as N, type OpenResourcesManager as O, type TranslationManager as T, type KnowledgeBase as a, type OpenResource as b, KnowledgeBaseSessionContext as c, KnowledgeBaseSessionProvider as d, type KnowledgeBaseSessionValue as e, useKnowledgeBaseSession as u };
@@ -2,8 +2,8 @@
2
2
  "use client";
3
3
  import {
4
4
  createHoverHandlers
5
- } from "./chunk-BQJWOK4C.mjs";
6
- import "./chunk-HNZOXH4L.mjs";
5
+ } from "./chunk-VN5NY4SN.mjs";
6
+ import "./chunk-OZICDVH7.mjs";
7
7
  import "./chunk-D4GAAQMM.mjs";
8
8
 
9
9
  // src/components/pdf-annotation/PdfAnnotationCanvas.tsx
@@ -487,4 +487,4 @@ function PdfAnnotationCanvas({
487
487
  export {
488
488
  PdfAnnotationCanvas
489
489
  };
490
- //# sourceMappingURL=PdfAnnotationCanvas.client-CW6SKH2U.mjs.map
490
+ //# sourceMappingURL=PdfAnnotationCanvas.client-CHDCGQBR.mjs.map
@@ -29,6 +29,7 @@ import { jsx as jsx2 } from "react/jsx-runtime";
29
29
  var ApiClientContext = createContext2(void 0);
30
30
  function ApiClientProvider({
31
31
  baseUrl: url,
32
+ tokenRefresher,
32
33
  children
33
34
  }) {
34
35
  const eventBus = useEventBus();
@@ -37,9 +38,10 @@ function ApiClientProvider({
37
38
  baseUrl: baseUrl(url),
38
39
  eventBus,
39
40
  // Use no timeout in test environment to avoid AbortController issues with ky + vitest
40
- ...process.env.NODE_ENV !== "test" && { timeout: 3e4 }
41
+ ...process.env.NODE_ENV !== "test" && { timeout: 3e4 },
42
+ ...tokenRefresher && { tokenRefresher }
41
43
  }),
42
- [url, eventBus]
44
+ [url, eventBus, tokenRefresher]
43
45
  );
44
46
  return /* @__PURE__ */ jsx2(ApiClientContext.Provider, { value: client, children });
45
47
  }
@@ -57,4 +59,4 @@ export {
57
59
  ApiClientProvider,
58
60
  useApiClient
59
61
  };
60
- //# sourceMappingURL=chunk-HNZOXH4L.mjs.map
62
+ //# sourceMappingURL=chunk-OZICDVH7.mjs.map
@@ -0,0 +1 @@
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, type TokenRefresher } from '@semiont/api-client';\nimport { useEventBus } from './EventBusContext';\n\nconst ApiClientContext = createContext<SemiontApiClient | undefined>(undefined);\n\nexport interface ApiClientProviderProps {\n baseUrl: string;\n /**\n * Optional 401-recovery hook. If provided, the api-client will retry\n * requests once with a fresh token when a 401 is encountered. The\n * frontend's protected layouts pass `useKnowledgeBaseSession().refreshActive`\n * here so the api-client can transparently recover from expired access tokens.\n */\n tokenRefresher?: TokenRefresher;\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 tokenRefresher,\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 ...(tokenRefresher && { tokenRefresher }),\n }),\n [url, eventBus, tokenRefresher]\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,wBAA6C;AA0ClD,gBAAAC,YAAA;AAvCJ,IAAM,mBAAmBC,eAA4C,MAAS;AAoBvE,SAAS,kBAAkB;AAAA,EAChC,SAAS;AAAA,EACT;AAAA,EACA;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,MACxD,GAAI,kBAAkB,EAAE,eAAe;AAAA,IACzC,CAAC;AAAA,IACD,CAAC,KAAK,UAAU,cAAc;AAAA,EAChC;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"]}