@topconsultnpm/sdkui-react 6.20.0-dev1.21 → 6.20.0-dev1.23

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.
@@ -4,18 +4,49 @@ import TMModal from './TMModal';
4
4
  import styled from 'styled-components';
5
5
  import { SDKUI_Localizator, TMLayoutWaitingContainer } from '../..';
6
6
  import { getButtonAttributes, getSelectedItem } from '../../helper/dcmtsHelper';
7
+ import { DeviceType, useDeviceType } from './TMDeviceProvider';
7
8
  const IframeContainer = styled.div `
8
9
  display: flex;
9
10
  height: 100%;
10
11
  flex-direction: column;
12
+ position: relative;
11
13
  `;
12
14
  const StyledIframe = styled.iframe `
13
15
  border: none;
14
16
  flex: 1;
15
17
  `;
18
+ const LoadingOverlay = styled.div `
19
+ position: absolute;
20
+ top: 0;
21
+ left: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ background-color: rgba(128, 128, 128, 0.3);
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ z-index: 1000;
29
+
30
+ &::after {
31
+ content: '';
32
+ width: 40px;
33
+ height: 40px;
34
+ border: 4px solid rgba(255, 255, 255, 0.3);
35
+ border-top-color: #fff;
36
+ border-radius: 50%;
37
+ animation: spin 0.8s linear infinite;
38
+ }
39
+
40
+ @keyframes spin {
41
+ to {
42
+ transform: rotate(360deg);
43
+ }
44
+ }
45
+ `;
16
46
  const TMCustomButton = (props) => {
17
47
  const { button, isModal = true, formData, selectedItems, onClose } = props;
18
48
  const { appName: scriptUrl, arguments: args } = button;
49
+ const Device = useDeviceType();
19
50
  const iframeRef = useRef(null);
20
51
  const attributes = useMemo(() => getButtonAttributes(args, formData, selectedItems), [args, formData, selectedItems]);
21
52
  const RunOnce = button.mode === "RunOnce";
@@ -28,6 +59,13 @@ const TMCustomButton = (props) => {
28
59
  const [waitPanelValue, setWaitPanelValue] = useState(0);
29
60
  const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(selectedItemsCount);
30
61
  const [abortController, setAbortController] = useState(undefined);
62
+ // Aggiungi timestamp all'URL per evitare cache
63
+ const iframeUrl = useMemo(() => {
64
+ if (!scriptUrl)
65
+ return '';
66
+ const separator = scriptUrl.includes('?') ? '&' : '?';
67
+ return `${scriptUrl}${separator}t=${Date.now()}`;
68
+ }, [scriptUrl]);
31
69
  const targetOrigin = useMemo(() => {
32
70
  if (!scriptUrl)
33
71
  return '*';
@@ -40,6 +78,7 @@ const TMCustomButton = (props) => {
40
78
  }
41
79
  }, [scriptUrl]);
42
80
  const handleLoad = () => setLoading(false);
81
+ const isMobile = Device === DeviceType.MOBILE;
43
82
  const handleError = () => {
44
83
  setLoading(false);
45
84
  setError(true);
@@ -81,7 +120,6 @@ const TMCustomButton = (props) => {
81
120
  useEffect(() => {
82
121
  if (loading || error)
83
122
  return;
84
- //if(error) clearTimeout(timeoutIframe);
85
123
  if (!RunOnce && selectedItemsCount > 0) {
86
124
  // esegui per ogni item selezionato
87
125
  const controller = new AbortController();
@@ -102,7 +140,6 @@ const TMCustomButton = (props) => {
102
140
  onClose?.();
103
141
  }, 2000);
104
142
  }
105
- //clearTimeout(timeoutIframe);
106
143
  }
107
144
  }, [loading, error, RunOnce]);
108
145
  useEffect(() => {
@@ -111,7 +148,7 @@ const TMCustomButton = (props) => {
111
148
  onClose?.();
112
149
  }
113
150
  }, []);
114
- const iframeContent = (_jsxs(IframeContainer, { style: !RunOnce ? { visibility: 'hidden' } : {}, children: [error && _jsx("div", { children: "Si \u00E8 verificato un errore nel caricamento del contenuto." }), !error && _jsx(StyledIframe, { ref: iframeRef, loading: 'lazy', onLoad: handleLoad, onError: handleError, src: scriptUrl })] }));
115
- return isModal && RunOnce ? (_jsx(TMModal, { title: button.title, width: '60%', height: '70%', resizable: true, expandable: true, onClose: onClose, children: iframeContent })) : !RunOnce && (_jsxs(_Fragment, { children: [_jsx(TMLayoutWaitingContainer, { showWaitPanel: showWaitPanel, waitPanelTitle: SDKUI_Localizator.CustomButtonAction, showWaitPanelPrimary: true, waitPanelTextPrimary: waitPanelText, waitPanelValuePrimary: waitPanelValue, waitPanelMaxValuePrimary: waitPanelMaxValue, showWaitPanelSecondary: false, isCancelable: true, abortController: abortController, children: undefined }), iframeContent] }));
151
+ const iframeContent = (_jsxs(IframeContainer, { style: !RunOnce ? { visibility: 'hidden' } : {}, children: [loading && _jsx(LoadingOverlay, {}), error && _jsx("div", { children: "Si \u00E8 verificato un errore nel caricamento del contenuto." }), !error && _jsx(StyledIframe, { ref: iframeRef, loading: 'lazy', onLoad: handleLoad, onError: handleError, src: iframeUrl })] }));
152
+ return isModal && RunOnce ? (_jsx(TMModal, { title: button.title, width: isMobile ? '95%' : '60%', height: isMobile ? '95%' : '70%', resizable: isMobile ? false : true, expandable: isMobile ? false : true, onClose: onClose, children: iframeContent })) : !RunOnce && (_jsxs(_Fragment, { children: [_jsx(TMLayoutWaitingContainer, { showWaitPanel: showWaitPanel, waitPanelTitle: SDKUI_Localizator.CustomButtonAction, showWaitPanelPrimary: true, waitPanelTextPrimary: waitPanelText, waitPanelValuePrimary: waitPanelValue, waitPanelMaxValuePrimary: waitPanelMaxValue, showWaitPanelSecondary: false, isCancelable: true, abortController: abortController, children: undefined }), iframeContent] }));
116
153
  };
117
154
  export default TMCustomButton;
@@ -15,35 +15,7 @@ import { StyledAnimatedComponentOpacity } from '../../base/Styled';
15
15
  import TMPanel from '../../base/TMPanel';
16
16
  import TMTooltip from '../../base/TMTooltip';
17
17
  import { ContextMenu } from '../../NewComponents/ContextMenu';
18
- let Document = null;
19
- let Page = null;
20
- let pdfjs = null;
21
- let isPdfLibraryLoaded = false;
22
- let loadingPromise = null;
23
- const loadPdfLibrary = async () => {
24
- if (isPdfLibraryLoaded)
25
- return;
26
- if (loadingPromise)
27
- return loadingPromise;
28
- loadingPromise = (async () => {
29
- try {
30
- const reactPdf = await import('react-pdf');
31
- Document = reactPdf.Document;
32
- Page = reactPdf.Page;
33
- pdfjs = reactPdf.pdfjs;
34
- pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
35
- isPdfLibraryLoaded = true;
36
- }
37
- catch (error) {
38
- console.error('Failed to load react-pdf library:', error);
39
- throw error;
40
- }
41
- finally {
42
- loadingPromise = null;
43
- }
44
- })();
45
- return loadingPromise;
46
- };
18
+ import TMPdfViewer from '../../../helper/TMPdfViewer';
47
19
  const ErrorContent = ({ error, isAbortError, onRetry }) => {
48
20
  if (isAbortError) {
49
21
  return (_jsx(StyledAnimatedComponentOpacity, { style: { width: '100%', height: '100%' }, children: _jsxs(StyledPanelStatusContainer, { children: [_jsx(IconCloseOutline, { fontSize: 92, color: TMColors.error }), _jsxs(StyledPreviewNotAvailable, { children: [_jsx("div", { children: error }), _jsx("div", { children: SDKUI_Localizator.PreviewNotAvailable })] }), _jsx(TMButton, { caption: SDKUI_Localizator.TryAgain, onClick: onRetry, showTooltip: false })] }) }));
@@ -166,53 +138,11 @@ export const TMFileViewer = ({ fileBlob, isResizingActive }) => {
166
138
  const [blobUrl, setBlobUrl] = useState(undefined);
167
139
  const [fileType, setFileType] = useState(undefined);
168
140
  const [formattedXml, setFormattedXml] = useState(undefined);
169
- const [isMobile, setIsMobile] = useState(false);
170
- const [numPages, setNumPages] = useState(0);
171
- const [pdfLibraryLoading, setPdfLibraryLoading] = useState(false);
172
- useEffect(() => {
173
- const checkIsMobile = () => {
174
- const userAgent = navigator.userAgent || navigator.vendor || window.opera;
175
- // Only detect actual mobile/tablet devices, NOT desktop browsers
176
- const isMobileDevice =
177
- // Traditional mobile detection (phones and tablets)
178
- /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent) ||
179
- // Additional Android tablet detection (covers tablets in landscape)
180
- /android.*tablet|android.*mobile/i.test(userAgent) ||
181
- // Touch-only devices (excludes laptops with touchscreen)
182
- (('ontouchstart' in window || navigator.maxTouchPoints > 0) &&
183
- !/Windows NT|Macintosh|Linux/.test(userAgent)) ||
184
- // Small screen mobile devices only
185
- (window.screen.width <= 768 && /Mobi|Android/i.test(userAgent));
186
- setIsMobile(isMobileDevice);
187
- };
188
- checkIsMobile();
189
- // Listen for orientation changes (important for tablets)
190
- window.addEventListener('orientationchange', checkIsMobile);
191
- window.addEventListener('resize', checkIsMobile);
192
- return () => {
193
- window.removeEventListener('orientationchange', checkIsMobile);
194
- window.removeEventListener('resize', checkIsMobile);
195
- };
196
- }, []);
197
141
  useEffect(() => {
198
142
  if (fileBlob) {
199
143
  const blobType = fileBlob.type;
200
144
  setFileType(blobType);
201
145
  setFormattedXml(undefined);
202
- // Load PDF library immediately if it's a PDF file on mobile
203
- if (isMobile && blobType === 'application/pdf' && !isPdfLibraryLoaded && !pdfLibraryLoading) {
204
- setPdfLibraryLoading(true);
205
- loadPdfLibrary()
206
- .then(() => {
207
- console.log('PDF library loaded successfully');
208
- })
209
- .catch((error) => {
210
- console.error('Failed to load PDF library:', error);
211
- })
212
- .finally(() => {
213
- setPdfLibraryLoading(false);
214
- });
215
- }
216
146
  const fileName = fileBlob.name || '';
217
147
  const fileExtension = fileName.split('.').pop()?.toLowerCase() || '';
218
148
  const isConfigFile = ['config', 'cfg'].includes(fileExtension);
@@ -269,36 +199,10 @@ export const TMFileViewer = ({ fileBlob, isResizingActive }) => {
269
199
  if (fileBlob.type.includes('image')) {
270
200
  return (_jsx(ImageViewer, { fileBlob: fileBlob, alt: '' }));
271
201
  }
272
- if (fileType === 'application/pdf' && isMobile) {
273
- if (!isPdfLibraryLoaded || pdfLibraryLoading || !Document || !Page) {
274
- return (_jsxs("div", { style: {
275
- display: 'flex',
276
- justifyContent: 'center',
277
- alignItems: 'center',
278
- height: '100%',
279
- flexDirection: 'column',
280
- gap: '10px'
281
- }, children: [_jsx(IconPreview, { fontSize: 64 }), _jsxs("div", { children: [SDKUI_Localizator.Loading, "..."] })] }));
282
- }
283
- return (_jsx(PDFViewerContainer, { children: _jsx(Document, { file: blobUrl, onLoadSuccess: ({ numPages }) => setNumPages(numPages), loading: _jsxs("div", { style: {
284
- display: 'flex',
285
- justifyContent: 'center',
286
- alignItems: 'center',
287
- height: '100%',
288
- flexDirection: 'column',
289
- gap: '10px'
290
- }, children: [_jsx(IconPreview, { fontSize: 64 }), _jsxs("div", { children: [SDKUI_Localizator.Loading, "..."] })] }), error: _jsxs("div", { style: {
291
- display: 'flex',
292
- justifyContent: 'center',
293
- alignItems: 'center',
294
- height: '100%',
295
- flexDirection: 'column',
296
- gap: '10px'
297
- }, children: [_jsx(IconCloseOutline, { fontSize: 64, color: TMColors.error }), _jsx("div", { children: "Errore nel caricamento del PDF" })] }), children: Array.from(new Array(numPages), (el, index) => (_jsx(Page, { pageNumber: index + 1, renderTextLayer: false, renderAnnotationLayer: false, width: window.innerWidth }, `page_${index + 1}`))) }) }));
202
+ if (fileType === 'application/pdf') {
203
+ return _jsx(TMPdfViewer, { pdfBlob: fileBlob, isResizingActive: isResizingActive, enableFitToWidth: true });
298
204
  }
299
- return (_jsx("iframe", { srcDoc: formattedXml ? `<html><body>${formattedXml}</body></html>` : undefined, src: !formattedXml
300
- ? (fileType === 'application/pdf' ? `${blobUrl}#view=FitH&scrollbar=1` : blobUrl)
301
- : undefined, title: "File Viewer", width: "100%", height: "100%", style: { border: 'none', zIndex: 0, pointerEvents: isResizingActive === true ? "none" : "auto" } }, blobUrl));
205
+ return (_jsx("iframe", { srcDoc: formattedXml ? `<html><body>${formattedXml}</body></html>` : undefined, src: !formattedXml ? blobUrl : undefined, title: "File Viewer", width: "100%", height: "100%", style: { border: 'none', zIndex: 0, pointerEvents: isResizingActive === true ? "none" : "auto" } }, blobUrl));
302
206
  };
303
207
  const ImageViewer = ({ fileBlob, alt = 'Image', className }) => {
304
208
  const containerRef = useRef(null);
@@ -360,7 +360,7 @@ const TMSearchResult = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, a
360
360
  const customButtonMenuItems = () => {
361
361
  const customButtonsItems = customButtonsLayout?.customButtons?.filter((customButton) => customButton.isForSearchResult && customButton.isForSearchResult > 0)
362
362
  .map((customButton) => ({
363
- icon: svgToString(TMImageLibrary({ imageID: customButton.glyphID, showPath: false })),
363
+ icon: svgToString(TMImageLibrary({ imageID: customButton.glyphID, showPath: true })),
364
364
  text: customButton.title || 'Bottone personalizzato',
365
365
  onClick: () => setCustomButton(customButton)
366
366
  }));
@@ -0,0 +1,8 @@
1
+ interface TMPdfViewerProps {
2
+ pdfBlob: Blob;
3
+ title?: string;
4
+ isResizingActive?: boolean;
5
+ enableFitToWidth?: boolean;
6
+ }
7
+ declare const TMPdfViewer: (props: TMPdfViewerProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default TMPdfViewer;
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState, useRef, useCallback } from "react";
3
+ import { pdfjs, Document, Page } from 'react-pdf';
4
+ import styled from "styled-components";
5
+ import { LoadIndicator } from 'devextreme-react/load-indicator';
6
+ import { IconCloseOutline } from "./TMIcons";
7
+ import { SDKUI_Localizator } from "./SDKUI_Localizator";
8
+ import { TMColors } from "../utils/theme";
9
+ pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
10
+ const PDFViewerContainer = styled.div `
11
+ width: 100%;
12
+ height: 100%;
13
+ overflow-y: auto;
14
+ overflow-x: hidden;
15
+ background-color: #f5f5f5;
16
+ position: relative;
17
+
18
+ .react-pdf__Document {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ gap: 10px;
23
+ padding: 10px 0;
24
+ }
25
+
26
+ .react-pdf__Page {
27
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
28
+ margin: 0 auto;
29
+ }
30
+
31
+ .react-pdf__Page__canvas {
32
+ max-width: 100%;
33
+ height: auto !important;
34
+ }
35
+ `;
36
+ const LoadingOverlay = styled.div `
37
+ position: absolute;
38
+ top: 0;
39
+ left: 0;
40
+ width: 100%;
41
+ height: 100%;
42
+ background-color: rgba(245, 245, 245, 0.95);
43
+ display: flex;
44
+ justify-content: center;
45
+ align-items: center;
46
+ z-index: 1000;
47
+ `;
48
+ const TMPdfViewer = (props) => {
49
+ const { pdfBlob, title = "Anteprima PDF", isResizingActive, enableFitToWidth = false } = props;
50
+ const [isMobile, setIsMobile] = useState(false);
51
+ const [totalPagesNumber, setTotalPagesNumber] = useState(0);
52
+ const [loadedPagesNumber, setLoadedPagesNumber] = useState(0);
53
+ const [visiblePages, setVisiblePages] = useState(new Set([1]));
54
+ const [pdfUrl, setPdfUrl] = useState("");
55
+ const observerRef = useRef(null);
56
+ useEffect(() => {
57
+ const checkIsMobile = () => {
58
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera;
59
+ // Detect actual mobile/tablet devices
60
+ const isMobileDevice =
61
+ // User agent detection (phones and tablets)
62
+ /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent) ||
63
+ // Media query for touch devices with coarse pointer (more reliable than screen width)
64
+ (window.matchMedia?.('(hover: none) and (pointer: coarse)').matches ?? false) ||
65
+ // Touch-capable devices excluding desktop OS
66
+ (navigator.maxTouchPoints > 1 && !/Win|Mac|Linux x86_64/i.test(userAgent));
67
+ setIsMobile(isMobileDevice);
68
+ };
69
+ checkIsMobile();
70
+ // Create URL for iframe
71
+ const url = URL.createObjectURL(pdfBlob);
72
+ setPdfUrl(url);
73
+ return () => {
74
+ if (url) {
75
+ URL.revokeObjectURL(url);
76
+ }
77
+ };
78
+ }, [pdfBlob]);
79
+ /**
80
+ * Callback per l'Intersection Observer che gestisce il lazy loading delle pagine PDF.
81
+ * Viene chiamato ogni volta che una pagina entra o esce dal viewport (area visibile).
82
+ *
83
+ * - Monitora quando gli elementi (pagine) diventano visibili
84
+ * - Aggiunge il numero di pagina al Set delle pagine visibili quando entra nel viewport
85
+ * - Il rootMargin di 500px carica le pagine prima che siano effettivamente visibili (preloading)
86
+ */
87
+ const pageObserverCallback = useCallback((entries) => {
88
+ entries.forEach(entry => {
89
+ const pageNum = parseInt(entry.target.getAttribute('data-page-number') || '0');
90
+ if (entry.isIntersecting) {
91
+ // Aggiunge la pagina al Set di quelle da renderizzare
92
+ setVisiblePages(prev => new Set([...prev, pageNum]));
93
+ }
94
+ });
95
+ }, []);
96
+ /**
97
+ * Crea e configura l'Intersection Observer per il lazy loading.
98
+ *
99
+ * Configurazione:
100
+ * - root: null → osserva rispetto al viewport del browser
101
+ * - rootMargin: '500px' → inizia il caricamento 500px prima che la pagina sia visibile (buffer)
102
+ * - threshold: 0.01 → trigger quando anche solo l'1% dell'elemento è visibile
103
+ *
104
+ * Benefici:
105
+ * - Non blocca l'UI durante il rendering
106
+ * - Carica solo le pagine necessarie
107
+ * - Migliora le performance su PDF con molte pagine
108
+ */
109
+ useEffect(() => {
110
+ observerRef.current = new IntersectionObserver(pageObserverCallback, {
111
+ root: null,
112
+ rootMargin: '500px',
113
+ threshold: 0.01
114
+ });
115
+ return () => {
116
+ // Cleanup: disconnette l'observer quando il componente viene smontato
117
+ observerRef.current?.disconnect();
118
+ };
119
+ }, [pageObserverCallback]);
120
+ // Use iframe for desktop, react-pdf for mobile
121
+ if (!isMobile && pdfUrl) {
122
+ return (_jsx(PDFViewerContainer, { children: _jsx("iframe", { src: `${pdfUrl}#${enableFitToWidth ? 'view=FitH&' : ''}scrollbar=1`, title: title, style: {
123
+ width: '100%',
124
+ height: '100%',
125
+ border: 'none',
126
+ zIndex: 0,
127
+ pointerEvents: isResizingActive === true ? "none" : "auto"
128
+ } }, pdfUrl) }));
129
+ }
130
+ return _jsxs(PDFViewerContainer, { children: [loadedPagesNumber === 0 && totalPagesNumber > 0 && _jsx(LoadingOverlay, { children: _jsxs("div", { style: {
131
+ display: 'flex',
132
+ justifyContent: 'center',
133
+ alignItems: 'center',
134
+ flexDirection: 'column',
135
+ gap: '10px'
136
+ }, children: [_jsx(LoadIndicator, { height: 60, width: 60 }), _jsxs("div", { children: [SDKUI_Localizator.Loading, "..."] })] }) }), _jsx("div", { style: { display: loadedPagesNumber > 0 && totalPagesNumber > 0 ? 'block' : 'none' }, children: _jsx(Document, { file: pdfBlob, onLoadSuccess: ({ numPages }) => {
137
+ setTotalPagesNumber(numPages);
138
+ setLoadedPagesNumber(0);
139
+ }, loading: _jsxs("div", { style: {
140
+ display: 'flex',
141
+ justifyContent: 'center',
142
+ alignItems: 'center',
143
+ height: '100%',
144
+ flexDirection: 'column',
145
+ gap: '10px'
146
+ }, children: [_jsx(LoadIndicator, { height: 60, width: 60 }), _jsxs("div", { children: [SDKUI_Localizator.Loading, "..."] })] }), error: _jsxs("div", { style: {
147
+ display: 'flex',
148
+ justifyContent: 'center',
149
+ alignItems: 'center',
150
+ height: '100%',
151
+ flexDirection: 'column',
152
+ gap: '10px'
153
+ }, children: [_jsx(IconCloseOutline, { fontSize: 64, color: TMColors.error }), _jsx("div", { children: "Errore nel caricamento del PDF" })] }), children: Array.from(new Array(totalPagesNumber), (el, index) => {
154
+ const pageNumber = index + 1;
155
+ const shouldRender = visiblePages.has(pageNumber);
156
+ return (_jsx("div", { "data-page-number": pageNumber, ref: (el) => {
157
+ if (el && observerRef.current) {
158
+ observerRef.current.observe(el);
159
+ }
160
+ }, style: {
161
+ minHeight: shouldRender ? 'auto' : '1000px',
162
+ display: 'flex',
163
+ justifyContent: 'center',
164
+ alignItems: 'center'
165
+ }, children: shouldRender && (_jsx(Page, { pageNumber: pageNumber, renderTextLayer: false, renderAnnotationLayer: false, width: Math.min(window.innerWidth - 40, 1200), loading: _jsx("div", { style: { padding: '20px' }, children: _jsx(LoadIndicator, { height: 40, width: 40 }) }), onLoadSuccess: () => {
166
+ setLoadedPagesNumber(prev => prev + 1);
167
+ } })) }, `page_${pageNumber}`));
168
+ }) }) })] });
169
+ };
170
+ export default TMPdfViewer;
@@ -9,6 +9,7 @@ export * from './queryHelper';
9
9
  export * from './TMUtils';
10
10
  export * from './TMCommandsContextMenu';
11
11
  export * from './TMConditionalWrapper';
12
+ export * from './TMPdfViewer';
12
13
  export * from './TMToppyMessage';
13
14
  export * from './GlobalStyles';
14
15
  export * from './checkinCheckoutManager';
@@ -9,6 +9,7 @@ export * from './queryHelper';
9
9
  export * from './TMUtils';
10
10
  export * from './TMCommandsContextMenu';
11
11
  export * from './TMConditionalWrapper';
12
+ export * from './TMPdfViewer';
12
13
  export * from './TMToppyMessage';
13
14
  export * from './GlobalStyles';
14
15
  export * from './checkinCheckoutManager';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.20.0-dev1.21",
3
+ "version": "6.20.0-dev1.23",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -1,4 +0,0 @@
1
- import React from 'react';
2
- import type { NotificationProps } from './types';
3
- declare const Notification: React.FC<NotificationProps>;
4
- export default Notification;
@@ -1,60 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from 'react';
3
- import * as S from './styles';
4
- const Notification = ({ title, message, mode = 'info', position = 'top-right', duration = 3000, closable = false, stopOnMouseEnter = true, hasProgress = true, onClose, }) => {
5
- const [state, setState] = useState({
6
- visible: true,
7
- isPaused: false,
8
- progress: 100,
9
- });
10
- const timeoutRef = useRef(undefined);
11
- const remainingTimeRef = useRef(duration);
12
- const pauseTimeRef = useRef(0);
13
- const closeNotification = () => {
14
- setState(prev => ({ ...prev, visible: false }));
15
- setTimeout(() => {
16
- onClose?.();
17
- }, 300);
18
- };
19
- useEffect(() => {
20
- // Set up auto-close timer
21
- timeoutRef.current = setTimeout(() => {
22
- closeNotification();
23
- }, duration);
24
- return () => {
25
- if (timeoutRef.current) {
26
- clearTimeout(timeoutRef.current);
27
- }
28
- };
29
- }, [duration]);
30
- const handleMouseEnter = () => {
31
- if (!stopOnMouseEnter)
32
- return;
33
- // Pause the timer
34
- if (timeoutRef.current) {
35
- clearTimeout(timeoutRef.current);
36
- pauseTimeRef.current = Date.now();
37
- }
38
- setState(prev => ({ ...prev, isPaused: true }));
39
- };
40
- const handleMouseLeave = () => {
41
- if (!stopOnMouseEnter)
42
- return;
43
- // Resume the timer with remaining time
44
- const pauseDuration = Date.now() - pauseTimeRef.current;
45
- remainingTimeRef.current = Math.max(0, remainingTimeRef.current - pauseDuration);
46
- timeoutRef.current = setTimeout(() => {
47
- closeNotification();
48
- }, remainingTimeRef.current);
49
- setState(prev => ({ ...prev, isPaused: false }));
50
- };
51
- const handleClose = (e) => {
52
- e.stopPropagation();
53
- if (timeoutRef.current) {
54
- clearTimeout(timeoutRef.current);
55
- }
56
- closeNotification();
57
- };
58
- return (_jsxs(S.NotificationContainer, { "$position": position, "$mode": mode, "$visible": state.visible, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "alert", "aria-live": "assertive", children: [_jsxs(S.NotificationContent, { children: [_jsx(S.NotificationTitle, { children: title }), _jsx(S.NotificationMessage, { children: message })] }), closable && (_jsx(S.CloseButton, { onClick: handleClose, "aria-label": "Close notification", children: "\u00D7" })), hasProgress && (_jsx(S.ProgressBar, { "$duration": duration, "$mode": mode, "$isPaused": state.isPaused }))] }));
59
- };
60
- export default Notification;
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- import type { NotificationPosition } from './types';
3
- interface NotificationContainerProps {
4
- position: NotificationPosition;
5
- children: React.ReactNode;
6
- }
7
- declare const NotificationContainer: React.FC<NotificationContainerProps>;
8
- export default NotificationContainer;
@@ -1,33 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import styled from 'styled-components';
3
- const Container = styled.div `
4
- position: fixed;
5
- z-index: 10002;
6
- display: flex;
7
- flex-direction: column;
8
- gap: 12px;
9
- pointer-events: none;
10
-
11
- ${props => {
12
- const isTop = props.$position.startsWith('top');
13
- const isBottom = props.$position.startsWith('bottom');
14
- const isLeft = props.$position.endsWith('left');
15
- const isRight = props.$position.endsWith('right');
16
- const isCenter = props.$position.endsWith('center');
17
- return `
18
- ${isTop ? 'top: 24px;' : ''}
19
- ${isBottom ? 'bottom: 24px;' : ''}
20
- ${isLeft ? 'left: 24px;' : ''}
21
- ${isRight ? 'right: 24px;' : ''}
22
- ${isCenter ? 'left: 50%; transform: translateX(-50%);' : ''}
23
- `;
24
- }}
25
-
26
- & > * {
27
- pointer-events: auto;
28
- }
29
- `;
30
- const NotificationContainer = ({ position, children }) => {
31
- return _jsx(Container, { "$position": position, children: children });
32
- };
33
- export default NotificationContainer;
@@ -1,2 +0,0 @@
1
- export { default } from './Notification';
2
- export * from './types';
@@ -1,2 +0,0 @@
1
- export { default } from './Notification';
2
- export * from './types';
@@ -1,21 +0,0 @@
1
- import type { NotificationMode, NotificationPosition } from './types';
2
- export declare const NotificationContainer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
3
- $position: NotificationPosition;
4
- $mode: NotificationMode;
5
- $visible: boolean;
6
- }>> & string;
7
- export declare const NotificationContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
8
- export declare const NotificationTitle: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
9
- export declare const NotificationMessage: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
10
- export declare const CloseButton: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
11
- export declare const ProgressBar: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("styled-components/dist/types").Substitute<import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
12
- ref?: ((instance: HTMLDivElement | null) => void | import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | import("react").RefObject<HTMLDivElement> | null | undefined;
13
- }>, {
14
- $duration: number;
15
- $mode: NotificationMode;
16
- $isPaused: boolean;
17
- }>, {
18
- $duration: number;
19
- $mode: NotificationMode;
20
- $isPaused: boolean;
21
- }>> & string;
@@ -1,180 +0,0 @@
1
- import styled, { keyframes } from 'styled-components';
2
- const slideInFromTop = keyframes `
3
- from {
4
- opacity: 0;
5
- transform: translateY(-100%);
6
- }
7
- to {
8
- opacity: 1;
9
- transform: translateY(0);
10
- }
11
- `;
12
- const slideInFromBottom = keyframes `
13
- from {
14
- opacity: 0;
15
- transform: translateY(100%);
16
- }
17
- to {
18
- opacity: 1;
19
- transform: translateY(0);
20
- }
21
- `;
22
- const slideOut = keyframes `
23
- from {
24
- opacity: 1;
25
- transform: scale(1);
26
- }
27
- to {
28
- opacity: 0;
29
- transform: scale(0.9);
30
- }
31
- `;
32
- const getModeColors = (mode) => {
33
- const colors = {
34
- success: {
35
- bg: '#10b981',
36
- bgDark: '#059669',
37
- border: '#34d399',
38
- text: '#ffffff',
39
- },
40
- error: {
41
- bg: '#ef4444',
42
- bgDark: '#dc2626',
43
- border: '#f87171',
44
- text: '#ffffff',
45
- },
46
- warning: {
47
- bg: '#f59e0b',
48
- bgDark: '#d97706',
49
- border: '#fbbf24',
50
- text: '#ffffff',
51
- },
52
- info: {
53
- bg: '#3b82f6',
54
- bgDark: '#2563eb',
55
- border: '#60a5fa',
56
- text: '#ffffff',
57
- },
58
- };
59
- return colors[mode];
60
- };
61
- export const NotificationContainer = styled.div `
62
- position: relative;
63
- z-index: 1;
64
- min-width: 320px;
65
- max-width: 420px;
66
- background: ${props => getModeColors(props.$mode).bg};
67
- border-radius: 12px;
68
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2),
69
- 0 4px 12px rgba(0, 0, 0, 0.15);
70
- padding: 16px 20px;
71
- animation: ${props => {
72
- if (!props.$visible)
73
- return slideOut;
74
- return props.$position.startsWith('top') ? slideInFromTop : slideInFromBottom;
75
- }} 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
76
- backdrop-filter: blur(10px);
77
- border: 2px solid ${props => getModeColors(props.$mode).border};
78
- color: ${props => getModeColors(props.$mode).text};
79
- overflow: hidden;
80
-
81
- [data-theme='dark'] & {
82
- background: ${props => getModeColors(props.$mode).bgDark};
83
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4),
84
- 0 4px 12px rgba(0, 0, 0, 0.3);
85
- }
86
-
87
- @media (max-width: 768px) {
88
- min-width: 280px;
89
- max-width: calc(100vw - 48px);
90
- }
91
- `;
92
- export const NotificationContent = styled.div `
93
- display: flex;
94
- flex-direction: column;
95
- gap: 6px;
96
- padding-right: 24px;
97
- `;
98
- export const NotificationTitle = styled.div `
99
- font-size: 16px;
100
- font-weight: 600;
101
- line-height: 1.4;
102
- letter-spacing: -0.01em;
103
- `;
104
- export const NotificationMessage = styled.div `
105
- font-size: 14px;
106
- font-weight: 400;
107
- line-height: 1.5;
108
- opacity: 0.95;
109
- `;
110
- export const CloseButton = styled.button `
111
- position: absolute;
112
- top: 12px;
113
- right: 12px;
114
- background: transparent;
115
- border: none;
116
- color: inherit;
117
- cursor: pointer;
118
- width: 24px;
119
- height: 24px;
120
- display: flex;
121
- align-items: center;
122
- justify-content: center;
123
- border-radius: 6px;
124
- transition: all 0.15s ease;
125
- font-size: 18px;
126
- line-height: 1;
127
- padding: 0;
128
- opacity: 0.8;
129
-
130
- &:hover {
131
- opacity: 1;
132
- background: rgba(255, 255, 255, 0.2);
133
- transform: scale(1.1);
134
- }
135
-
136
- &:active {
137
- transform: scale(0.95);
138
- }
139
-
140
- &:focus {
141
- outline: 2px solid rgba(255, 255, 255, 0.5);
142
- outline-offset: 2px;
143
- }
144
- `;
145
- export const ProgressBar = styled.div.attrs(props => ({
146
- style: {
147
- animationDuration: `${props.$duration}ms`,
148
- },
149
- })) `
150
- position: absolute;
151
- bottom: 0;
152
- left: 0;
153
- height: 4px;
154
- width: 100%;
155
- background: ${props => getModeColors(props.$mode).border};
156
- border-radius: 0 0 0 10px;
157
- box-shadow: 0 0 8px ${props => getModeColors(props.$mode).border};
158
- transform-origin: left;
159
- animation: progress-shrink linear forwards;
160
- animation-play-state: ${props => props.$isPaused ? 'paused' : 'running'};
161
-
162
- @keyframes progress-shrink {
163
- from {
164
- transform: scaleX(1);
165
- }
166
- to {
167
- transform: scaleX(0);
168
- }
169
- }
170
-
171
- &::after {
172
- content: '';
173
- position: absolute;
174
- top: 0;
175
- right: 0;
176
- width: 20px;
177
- height: 100%;
178
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4));
179
- }
180
- `;
@@ -1,18 +0,0 @@
1
- export type NotificationMode = 'warning' | 'info' | 'error' | 'success';
2
- export type NotificationPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
3
- export interface NotificationProps {
4
- title: string;
5
- message: string;
6
- mode?: NotificationMode;
7
- position?: NotificationPosition;
8
- duration?: number;
9
- closable?: boolean;
10
- stopOnMouseEnter?: boolean;
11
- hasProgress?: boolean;
12
- onClose?: () => void;
13
- }
14
- export interface NotificationState {
15
- visible: boolean;
16
- isPaused: boolean;
17
- progress: number;
18
- }
@@ -1 +0,0 @@
1
- export {};