@topconsultnpm/sdkui-react 6.19.0-dev2.52 → 6.19.0-dev2.54

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.
@@ -110,6 +110,6 @@ const TMModal = ({ resizable = true, expandable = false, isModal = true, title =
110
110
  onClick: () => setIsFullScreen(!isFullScreen)
111
111
  }
112
112
  }
113
- ] : undefined, children: _jsxs(TMLayoutContainer, { children: [toolbar && (_jsx(TMLayoutItem, { height: "40px", children: _jsx(StyledModalToolbar, { children: toolbar }) })), _jsx(TMLayoutItem, { height: toolbar ? 'calc(100% - 50px)' : 'calc(100% - 10px)', children: _jsx(TMCard, { showBorder: false, padding: false, scrollY: true, children: children }) })] }) })) : (_jsxs(StyledModal, { "$isModal": isModal, className: "temp-modal", "$fontSize": fontSize, "$width": initialWidth, "$height": initialHeight, children: [toolbar ? _jsx(StyledModalToolbar, { children: toolbar }) : _jsx(_Fragment, {}), _jsx(StyledModalContext, { children: children })] })) }));
113
+ ] : undefined, children: _jsxs(TMLayoutContainer, { children: [toolbar && (_jsx(TMLayoutItem, { height: "40px", children: _jsx(StyledModalToolbar, { children: toolbar }) })), _jsx(TMLayoutItem, { height: toolbar ? 'calc(100% - 40px)' : '100%', children: _jsx(TMCard, { showBorder: false, padding: false, scrollY: true, children: children }) })] }) })) : (_jsxs(StyledModal, { "$isModal": isModal, className: "temp-modal", "$fontSize": fontSize, "$width": initialWidth, "$height": initialHeight, children: [toolbar ? _jsx(StyledModalToolbar, { children: toolbar }) : _jsx(_Fragment, {}), _jsx(StyledModalContext, { children: children })] })) }));
114
114
  };
115
115
  export default TMModal;
@@ -33,7 +33,8 @@ const StyledExeptionToolbar = styled.div `
33
33
  background-color: white;
34
34
  `;
35
35
  const StyledMessageToolbar = styled.div `
36
- position: relative;
36
+ position: sticky;
37
+ bottom: 0;
37
38
  display: flex;
38
39
  flex-direction: row;
39
40
  justify-content: flex-end;
@@ -95,6 +96,7 @@ const ResponsiveMessageContainer = styled.div `
95
96
  height: 100%;
96
97
  width: 100%;
97
98
  background-color: #ffffff;
99
+ overflow: hidden;
98
100
  `;
99
101
  const ResponsiveMessageContent = styled.div `
100
102
  flex: 1;
@@ -105,6 +107,7 @@ const ResponsiveMessageContent = styled.div `
105
107
  gap: clamp(8px, 2vw, 20px);
106
108
  padding: clamp(8px, 3vw, 25px) clamp(5px, 2vw, 30px);
107
109
  min-height: clamp(80px, 15vh, 120px);
110
+ overflow: auto;
108
111
 
109
112
  @media (min-width: 300px) {
110
113
  flex-direction: ${props => props.$isMobile ? 'column' : 'row'};
@@ -16,6 +16,8 @@ interface ToppyDraggableHelpCenterProps {
16
16
  onToppyImageClick?: () => void;
17
17
  /** Visibilità del componente */
18
18
  isVisible?: boolean;
19
+ /** Se true, renderizza Toppy nel document.body tramite Portal invece che nel parent */
20
+ usePortal?: boolean;
19
21
  }
20
22
  /**
21
23
  * Componente ToppyDraggableHelpCenter
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect } from 'react';
3
+ import ReactDOM from 'react-dom';
3
4
  import styled from 'styled-components';
4
5
  import Toppy from '../../../assets/Toppy-generico.png';
5
6
  import { DeviceType } from '../../base/TMDeviceProvider';
6
7
  import ToppySpeechBubble from './ToppySpeechBubble';
8
+ import { SDKUI_Localizator } from '../../../helper';
9
+ import { IconWindowMaximize } from '../../../helper/TMIcons';
7
10
  /**
8
11
  * Styled component per il contenitore di Toppy
9
12
  * Gestisce il posizionamento, le dimensioni e le animazioni
@@ -25,7 +28,7 @@ const ToppyButton = styled.div.attrs((props) => ({
25
28
  })) `
26
29
  /* Visibilità controllata dalla prop */
27
30
  display: ${(props) => (props.$isVisible ? 'flex' : 'none')};
28
- position: absolute;
31
+ position: ${(props) => props.$usePortal ? 'fixed' : 'absolute'};
29
32
 
30
33
  /* Posizionamento di default quando non è draggato (x e y non sono definiti) */
31
34
  ${(props) => props.$x === undefined || props.$y === undefined
@@ -106,6 +109,45 @@ const ToppyButton = styled.div.attrs((props) => ({
106
109
  }
107
110
  }
108
111
  `;
112
+ /**
113
+ * Pulsante di espansione quando Toppy è minimizzato
114
+ */
115
+ const ExpandButton = styled.button `
116
+ position: absolute;
117
+ top: -8px;
118
+ right: -8px;
119
+ width: 24px;
120
+ height: 24px;
121
+ border-radius: 50%;
122
+ border: 2px solid rgba(255, 255, 255, 0.9);
123
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
124
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 20px rgba(118, 75, 162, 0.2);
125
+ cursor: pointer;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
130
+ z-index: 10001;
131
+ color: white;
132
+ font-size: 14px;
133
+
134
+ &:hover {
135
+ background: linear-gradient(135deg, #764ba2 0%, #f093fb 100%);
136
+ box-shadow: 0 6px 20px rgba(118, 75, 162, 0.6), 0 0 30px rgba(240, 147, 251, 0.4);
137
+ border-color: rgba(255, 255, 255, 1);
138
+ transform: scale(1.1);
139
+ }
140
+
141
+ &:active {
142
+ transform: scale(0.95);
143
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
144
+ }
145
+
146
+ &:focus {
147
+ outline: none;
148
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 0 3px rgba(102, 126, 234, 0.2);
149
+ }
150
+ `;
109
151
  /**
110
152
  * Overlay trasparente per bloccare le interazioni durante il drag
111
153
  * Previene problemi di performance con iframe e altri elementi interattivi
@@ -126,7 +168,7 @@ const DragOverlay = styled.div `
126
168
  * in una speech bubble. Il componente può essere trascinato all'interno del suo
127
169
  * contenitore e può essere collassato/espanso con un doppio click.
128
170
  */
129
- const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onToppyImageClick, initialIsCollapsed, isVisible = true, }) => {
171
+ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onToppyImageClick, initialIsCollapsed, isVisible = true, usePortal = false, }) => {
130
172
  // Ref per il contenitore principale
131
173
  const buttonRef = useRef(null);
132
174
  // Stato per controllare se il componente è collassato o espanso
@@ -140,6 +182,9 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
140
182
  // Ref e stato per tracciare le dimensioni della speech bubble
141
183
  const bubbleRef = useRef(null);
142
184
  const [bubbleSize, setBubbleSize] = useState({ width: 0, height: 0 });
185
+ // Ref per tracciare il dragging dell'ExpandButton
186
+ const isExpandButtonDraggingRef = useRef(false);
187
+ const expandButtonMouseDownPosRef = useRef(null);
143
188
  const isMobile = deviceType === DeviceType.MOBILE;
144
189
  /**
145
190
  * Effect per aggiornare le dimensioni della bubble quando cambia il contenuto
@@ -153,57 +198,112 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
153
198
  }
154
199
  }, [content, isCollapsed]);
155
200
  /**
156
- * Effect per resettare la posizione quando il parent o la finestra vengono ridimensionati.
157
- * Questo previene che il componente finisca fuori dai bordi dopo un resize.
201
+ * Effect per verificare e aggiustare la posizione quando cambia lo stato di collassamento
158
202
  */
159
203
  useEffect(() => {
160
- if (!buttonRef.current)
204
+ if (!buttonRef.current || !position)
161
205
  return;
162
- const parent = buttonRef.current.offsetParent;
163
- if (!parent)
206
+ const rect = buttonRef.current.getBoundingClientRect();
207
+ // Spazio extra occupato dalla bubble quando non è collassato
208
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
209
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
210
+ let minX = 0;
211
+ let maxX;
212
+ let maxY;
213
+ const bubbleBuffer = 5;
214
+ if (usePortal) {
215
+ // Calcola i limiti usando le dimensioni del viewport
216
+ maxX = window.innerWidth - rect.width;
217
+ maxY = window.innerHeight - rect.height + 20;
218
+ }
219
+ else {
220
+ // Calcola i limiti usando le dimensioni del parent
221
+ const parent = buttonRef.current.offsetParent;
222
+ if (!parent)
223
+ return;
224
+ const parentRect = parent.getBoundingClientRect();
225
+ maxX = parentRect.width - rect.width;
226
+ maxY = parentRect.height - rect.height + 20;
227
+ }
228
+ if (!isCollapsed) {
229
+ if (align === 'right') {
230
+ minX = Math.max(0, extraWidth - rect.width);
231
+ }
232
+ else {
233
+ maxX = Math.min(maxX, (usePortal ? window.innerWidth : buttonRef.current.offsetParent?.getBoundingClientRect().width || 0) - extraWidth);
234
+ }
235
+ }
236
+ // Verifica se la posizione corrente è fuori dai limiti
237
+ const isOutOfBounds = position.x < minX ||
238
+ position.x > maxX ||
239
+ position.y < extraHeight + bubbleBuffer ||
240
+ position.y > maxY;
241
+ // Se è fuori dai limiti, aggiusta la posizione
242
+ if (isOutOfBounds) {
243
+ const adjustedX = Math.max(minX, Math.min(position.x, maxX));
244
+ const adjustedY = Math.max(extraHeight + bubbleBuffer, Math.min(position.y, maxY));
245
+ setPosition({ x: adjustedX, y: adjustedY });
246
+ }
247
+ }, [isCollapsed, bubbleSize, usePortal, align]);
248
+ /**
249
+ * Effect per resettare la posizione quando il parent o la finestra vengono ridimensionati
250
+ */
251
+ useEffect(() => {
252
+ if (!buttonRef.current || !position)
164
253
  return;
165
- // Funzione per verificare e aggiustare la posizione dopo un resize
166
254
  const handleResize = () => {
167
255
  if (!buttonRef.current || !position)
168
256
  return;
169
- const parentRect = parent.getBoundingClientRect();
170
257
  const rect = buttonRef.current.getBoundingClientRect();
171
- // Spazio extra occupato dalla bubble quando non è collassato
172
258
  const extraHeight = !isCollapsed ? bubbleSize.height : 0;
173
259
  const extraWidth = !isCollapsed ? bubbleSize.width : 0;
174
- // Calcola i nuovi limiti
175
260
  let minX = 0;
176
- let maxX = parentRect.width - rect.width;
261
+ let maxX;
262
+ let maxY;
263
+ const bubbleBuffer = 5;
264
+ if (usePortal) {
265
+ maxX = window.innerWidth - rect.width;
266
+ maxY = window.innerHeight - rect.height + 20;
267
+ }
268
+ else {
269
+ const parent = buttonRef.current.offsetParent;
270
+ if (!parent)
271
+ return;
272
+ const parentRect = parent.getBoundingClientRect();
273
+ maxX = parentRect.width - rect.width;
274
+ maxY = parentRect.height - rect.height + 20;
275
+ }
177
276
  if (!isCollapsed) {
178
277
  if (align === 'right') {
179
278
  minX = Math.max(0, extraWidth - rect.width);
180
279
  }
181
280
  else {
182
- maxX = Math.min(maxX, parentRect.width - extraWidth);
281
+ maxX = Math.min(maxX, (usePortal ? window.innerWidth : buttonRef.current.offsetParent?.getBoundingClientRect().width || 0) - extraWidth);
183
282
  }
184
283
  }
185
- const bubbleBuffer = 5; // Buffer per evitare che la bubble esca leggermente dai bordi
186
- const maxY = parentRect.height - rect.height + 20; // +20px per permettere bottom: -20px
187
- // Verifica se la posizione corrente è fuori dai limiti
188
284
  const isOutOfBounds = position.x < minX ||
189
285
  position.x > maxX ||
190
286
  position.y < extraHeight + bubbleBuffer ||
191
287
  position.y > maxY;
192
- // Se è fuori dai limiti, resetta alla posizione default
193
288
  if (isOutOfBounds) {
194
289
  setPosition(null);
195
290
  }
196
291
  };
197
- // Observer per monitorare i cambiamenti di dimensione del parent
198
- const resizeObserver = new ResizeObserver(handleResize);
199
- resizeObserver.observe(parent);
200
- // Listener per il resize della finestra
292
+ // Observer per il parent se non usa Portal
293
+ let resizeObserver;
294
+ if (!usePortal) {
295
+ const parent = buttonRef.current.offsetParent;
296
+ if (parent) {
297
+ resizeObserver = new ResizeObserver(handleResize);
298
+ resizeObserver.observe(parent);
299
+ }
300
+ }
201
301
  window.addEventListener('resize', handleResize);
202
302
  return () => {
203
- resizeObserver.disconnect();
303
+ resizeObserver?.disconnect();
204
304
  window.removeEventListener('resize', handleResize);
205
305
  };
206
- }, [position, isCollapsed, bubbleSize, align]);
306
+ }, [position, isCollapsed, bubbleSize, align, usePortal]);
207
307
  /**
208
308
  * Effect per impostare automaticamente lo stato collassato su dispositivi mobile
209
309
  * se non è stato specificato un valore iniziale.
@@ -218,7 +318,7 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
218
318
  * Chiamato dal doppio click sul componente
219
319
  */
220
320
  const toggleCollapse = (e) => {
221
- e.stopPropagation();
321
+ e?.stopPropagation();
222
322
  setIsCollapsed(!isCollapsed);
223
323
  onToppyImageClick?.();
224
324
  };
@@ -231,9 +331,6 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
231
331
  if (!buttonRef.current)
232
332
  return;
233
333
  const rect = buttonRef.current.getBoundingClientRect();
234
- const parentRect = buttonRef.current.offsetParent?.getBoundingClientRect();
235
- if (!parentRect)
236
- return;
237
334
  // Calcola l'offset tra il punto di click e l'angolo superiore sinistro del componente
238
335
  dragOffset.current = {
239
336
  x: e.clientX - rect.left,
@@ -244,42 +341,56 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
244
341
  };
245
342
  /**
246
343
  * Gestisce il movimento durante il trascinamento
247
- * Calcola la nuova posizione rispettando i limiti del parent container
344
+ * Calcola la nuova posizione rispettando i limiti del parent o viewport
248
345
  * e tenendo conto delle dimensioni della speech bubble quando espansa
249
346
  */
250
347
  const handleMouseMove = (e) => {
251
348
  if (!isDragging || !buttonRef.current)
252
349
  return;
253
- const parentRect = buttonRef.current.offsetParent?.getBoundingClientRect();
254
- if (!parentRect)
255
- return;
256
350
  const rect = buttonRef.current.getBoundingClientRect();
257
351
  // Spazio extra occupato dalla bubble quando non è collassato
258
352
  const extraHeight = !isCollapsed ? bubbleSize.height : 0;
259
353
  const extraWidth = !isCollapsed ? bubbleSize.width : 0;
260
- // Calcola i limiti orizzontali considerando la bubble
261
354
  let minX = 0;
262
- let maxX = parentRect.width - rect.width;
263
- if (!isCollapsed) {
264
- if (align === 'right') {
265
- // La bubble si estende verso sinistra: aumenta il limite minimo
266
- // per evitare che la bubble esca dal bordo sinistro
267
- minX = Math.max(0, extraWidth - rect.width);
355
+ let maxX;
356
+ let maxY;
357
+ let newX;
358
+ let newY;
359
+ const bubbleBuffer = 5;
360
+ if (usePortal) {
361
+ // Calcola i limiti usando il viewport
362
+ maxX = window.innerWidth - rect.width;
363
+ maxY = window.innerHeight - rect.height + 20;
364
+ if (!isCollapsed) {
365
+ if (align === 'right') {
366
+ minX = Math.max(0, extraWidth - rect.width);
367
+ }
368
+ else {
369
+ maxX = Math.min(maxX, window.innerWidth - extraWidth);
370
+ }
268
371
  }
269
- else {
270
- // La bubble si estende verso destra: riduce il limite massimo
271
- // per evitare che la bubble esca dal bordo destro
272
- maxX = Math.min(maxX, parentRect.width - extraWidth);
372
+ newX = Math.max(minX, Math.min(e.clientX - dragOffset.current.x, maxX));
373
+ newY = Math.max(extraHeight + bubbleBuffer, Math.min(e.clientY - dragOffset.current.y, maxY));
374
+ }
375
+ else {
376
+ // Calcola i limiti usando il parent
377
+ const parent = buttonRef.current.offsetParent;
378
+ if (!parent)
379
+ return;
380
+ const parentRect = parent.getBoundingClientRect();
381
+ maxX = parentRect.width - rect.width;
382
+ maxY = parentRect.height - rect.height + 20;
383
+ if (!isCollapsed) {
384
+ if (align === 'right') {
385
+ minX = Math.max(0, extraWidth - rect.width);
386
+ }
387
+ else {
388
+ maxX = Math.min(maxX, parentRect.width - extraWidth);
389
+ }
273
390
  }
391
+ newX = Math.max(minX, Math.min(e.clientX - parentRect.left - dragOffset.current.x, maxX));
392
+ newY = Math.max(extraHeight + bubbleBuffer, Math.min(e.clientY - parentRect.top - dragOffset.current.y, maxY));
274
393
  }
275
- // Calcola la nuova posizione X rispettando i limiti
276
- const newX = Math.max(minX, Math.min(e.clientX - parentRect.left - dragOffset.current.x, maxX));
277
- // Calcola la nuova posizione Y rispettando i limiti
278
- // Il limite superiore tiene conto dell'altezza della bubble + buffer di sicurezza
279
- // Il limite inferiore include 20px extra per permettere bottom: -20px
280
- const bubbleBuffer = 5; // Buffer per evitare che la bubble esca leggermente dai bordi
281
- const maxY = parentRect.height - rect.height + 20;
282
- const newY = Math.max(extraHeight + bubbleBuffer, Math.min(e.clientY - parentRect.top - dragOffset.current.y, maxY));
283
394
  setPosition({ x: newX, y: newY });
284
395
  };
285
396
  /**
@@ -319,6 +430,30 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
319
430
  }, [isDragging]);
320
431
  // Renderizza l'overlay solo durante il drag
321
432
  const renderDragOverlay = isDragging && _jsx(DragOverlay, {});
322
- return (_jsxs(_Fragment, { children: [renderDragOverlay, _jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, "$isMobile": isMobile, onMouseDown: !isMobile ? handleMouseDown : undefined, onContextMenu: (e) => e.preventDefault(), onDoubleClick: !isMobile ? toggleCollapse : undefined, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, children: content })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false, onClick: isMobile ? toggleCollapse : undefined })] })] }));
433
+ const toppyContent = (_jsxs(_Fragment, { children: [renderDragOverlay, _jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$usePortal": usePortal, onMouseDown: !isMobile ? handleMouseDown : undefined, onContextMenu: (e) => e.preventDefault(), onDoubleClick: !isMobile ? toggleCollapse : undefined, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, onClose: toggleCollapse, children: content })), (content && isCollapsed) && (_jsx(ExpandButton, { onMouseDown: (e) => {
434
+ isExpandButtonDraggingRef.current = false;
435
+ expandButtonMouseDownPosRef.current = { x: e.clientX, y: e.clientY };
436
+ }, onMouseMove: (e) => {
437
+ if (!expandButtonMouseDownPosRef.current)
438
+ return;
439
+ const deltaX = Math.abs(e.clientX - expandButtonMouseDownPosRef.current.x);
440
+ const deltaY = Math.abs(e.clientY - expandButtonMouseDownPosRef.current.y);
441
+ // Considera drag solo se il movimento supera 3px
442
+ if (deltaX > 3 || deltaY > 3) {
443
+ isExpandButtonDraggingRef.current = true;
444
+ }
445
+ }, onMouseUp: () => {
446
+ expandButtonMouseDownPosRef.current = null;
447
+ }, onClick: (e) => {
448
+ if (isExpandButtonDraggingRef.current) {
449
+ e.stopPropagation();
450
+ e.preventDefault();
451
+ return;
452
+ }
453
+ e.stopPropagation();
454
+ toggleCollapse(e);
455
+ }, onContextMenu: (e) => e.preventDefault(), "aria-label": SDKUI_Localizator.Maximize, title: SDKUI_Localizator.Maximize, type: "button", children: _jsx(IconWindowMaximize, {}) })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false, onClick: isMobile ? toggleCollapse : undefined })] })] }));
456
+ // Renderizza nel document.body usando un Portal se usePortal è true, altrimenti renderizza normalmente
457
+ return usePortal ? ReactDOM.createPortal(toppyContent, document.body) : toppyContent;
323
458
  };
324
459
  export default ToppyDraggableHelpCenter;
@@ -3,6 +3,7 @@ interface ToppySpeechBubbleProps {
3
3
  align?: 'left' | 'right';
4
4
  children: React.ReactNode;
5
5
  className?: string;
6
+ onClose?: () => void;
6
7
  }
7
8
  declare const ToppySpeechBubble: React.ForwardRefExoticComponent<ToppySpeechBubbleProps & React.RefAttributes<HTMLDivElement>>;
8
9
  export default ToppySpeechBubble;
@@ -1,6 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { forwardRef } from 'react';
3
3
  import styled from 'styled-components';
4
+ import { SDKUI_Localizator } from '../../../helper';
5
+ import { IconCloseOutline } from '../../../helper/TMIcons';
4
6
  // Styled component
5
7
  const Bubble = styled.div `
6
8
  position: absolute;
@@ -46,8 +48,70 @@ const Bubble = styled.div `
46
48
  z-index: 2;
47
49
  }
48
50
  `;
51
+ const CloseButton = styled.button `
52
+ position: absolute;
53
+ top: -8px;
54
+ right: -8px;
55
+ width: 24px;
56
+ height: 24px;
57
+ border-radius: 50%;
58
+ border: 2px solid rgba(255, 255, 255, 0.9);
59
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
60
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 20px rgba(118, 75, 162, 0.2);
61
+ cursor: pointer;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
66
+ z-index: 10001;
67
+ color: white;
68
+ font-size: 14px;
69
+
70
+ &:hover {
71
+ background: linear-gradient(135deg, #764ba2 0%, #f093fb 100%);
72
+ box-shadow: 0 6px 20px rgba(118, 75, 162, 0.6), 0 0 30px rgba(240, 147, 251, 0.4);
73
+ border-color: rgba(255, 255, 255, 1);
74
+ }
75
+
76
+ &:active {
77
+ transform: scale(0.95);
78
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
79
+ }
80
+
81
+ &:focus {
82
+ outline: none;
83
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 0 0 3px rgba(102, 126, 234, 0.2);
84
+ }
85
+ `;
49
86
  // Componente con forwardRef
50
- const ToppySpeechBubble = forwardRef(({ align = 'right', children, className }, ref) => {
51
- return (_jsx(Bubble, { ref: ref, "$align": align, className: className, children: children }));
87
+ const ToppySpeechBubble = forwardRef(({ align = 'right', children, className, onClose }, ref) => {
88
+ const isDraggingRef = React.useRef(false);
89
+ const mouseDownPosRef = React.useRef(null);
90
+ const handleMouseDown = (e) => {
91
+ isDraggingRef.current = false;
92
+ mouseDownPosRef.current = { x: e.clientX, y: e.clientY };
93
+ };
94
+ const handleMouseMove = (e) => {
95
+ if (!mouseDownPosRef.current)
96
+ return;
97
+ const deltaX = Math.abs(e.clientX - mouseDownPosRef.current.x);
98
+ const deltaY = Math.abs(e.clientY - mouseDownPosRef.current.y);
99
+ // Considera drag solo se il movimento supera 3px
100
+ if (deltaX > 3 || deltaY > 3) {
101
+ isDraggingRef.current = true;
102
+ }
103
+ };
104
+ const handleClick = (e) => {
105
+ if (isDraggingRef.current) {
106
+ e.stopPropagation();
107
+ e.preventDefault();
108
+ return;
109
+ }
110
+ onClose?.();
111
+ };
112
+ const handleMouseUp = () => {
113
+ mouseDownPosRef.current = null;
114
+ };
115
+ return (_jsxs(Bubble, { ref: ref, "$align": align, className: className, children: [onClose && (_jsx(CloseButton, { onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onClick: handleClick, "aria-label": SDKUI_Localizator.Minimize, title: SDKUI_Localizator.Minimize, type: "button", children: _jsx(IconCloseOutline, {}) })), children] }));
52
116
  });
53
117
  export default ToppySpeechBubble;
@@ -1353,7 +1353,7 @@ const TMDcmtForm = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTa
1353
1353
  isEditable: true,
1354
1354
  value: FormulaHelper.addFormulaTag(newFormula.expression)
1355
1355
  }));
1356
- } }), showApprovePopup && _jsx(WorkFlowApproveRejectPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, isReject: 0, onClose: () => setShowApprovePopup(false) }), showRejectPopup && _jsx(WorkFlowApproveRejectPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, isReject: 1, onClose: () => setShowRejectPopup(false) }), showReAssignPopup && _jsx(WorkFlowReAssignPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, onClose: () => setShowReAssignPopup(false) }), showMoreInfoPopup && _jsx(WorkFlowMoreInfoPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, onClose: () => setShowMoreInfoPopup(false) }), (isModal && onClose) && _jsx("div", { id: "TMDcmtFormShowConfirmForClose-" + id })] }), (showToppyForApprove || showToppyForCompleteMoreInfo || showToppyForReferences) && (_jsx(ToppyDraggableHelpCenter, { initialIsCollapsed: false, deviceType: deviceType, content: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '10px' }, children: [showToppyForApprove && (workItems.length === 1 ?
1356
+ } }), showApprovePopup && _jsx(WorkFlowApproveRejectPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, isReject: 0, onClose: () => setShowApprovePopup(false) }), showRejectPopup && _jsx(WorkFlowApproveRejectPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, isReject: 1, onClose: () => setShowRejectPopup(false) }), showReAssignPopup && _jsx(WorkFlowReAssignPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, onClose: () => setShowReAssignPopup(false) }), showMoreInfoPopup && _jsx(WorkFlowMoreInfoPopUp, { deviceType: deviceType, onCompleted: handleWFOperationCompleted, TID: approvalVID, DID: DID, onClose: () => setShowMoreInfoPopup(false) }), (isModal && onClose) && _jsx("div", { id: "TMDcmtFormShowConfirmForClose-" + id })] }), _jsx(ToppyDraggableHelpCenter, { initialIsCollapsed: false, deviceType: deviceType, isVisible: showToppyForApprove || showToppyForCompleteMoreInfo || showToppyForReferences, content: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '10px' }, children: [showToppyForApprove && (workItems.length === 1 ?
1357
1357
  _jsx(WorkFlowOperationButtons, { deviceType: deviceType, onApprove: () => setShowApprovePopup(true), onSignApprove: handleSignApprove, onReject: () => setShowRejectPopup(true), onReAssign: () => setShowReAssignPopup(true), onMoreInfo: () => setShowMoreInfoPopup(true), dtd: fromDTD })
1358
1358
  :
1359
1359
  _jsxs("div", { style: { padding: 10, color: 'white', maxWidth: '180px', borderRadius: 10, background: '#1B1464 0% 0% no-repeat padding-box', border: '1px solid #FFFFFF' }, children: [`Devi approvare ${workItems.length} workitem(s) per questo documento.`, `Vai alla sezione di approvazione.`] })), showToppyForCompleteMoreInfo && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: 10, color: 'white', maxWidth: '180px', borderRadius: 10, background: '#1B1464 0% 0% no-repeat padding-box', border: '1px solid #FFFFFF' }, children: `${SDKUI_Localizator.MoreInfoCompleteRequestSentBy} ${taskMoreInfo?.fromName}!` }), _jsx(TMButton, { caption: SDKUI_Localizator.CommentAndComplete, color: 'success', showTooltip: false, onClick: () => {
@@ -1368,7 +1368,7 @@ const TMDcmtForm = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTa
1368
1368
  backgroundColor: 'rgba(255,255,255,0.2)',
1369
1369
  margin: '6px 0'
1370
1370
  } })), _jsxs(StyledReferenceButton, { onClick: () => handleNavigateToReference(ref), children: [_jsx("span", { children: label }), _jsx("span", { children: `"${ref.objName}"` })] }, `ref-${index}-${ref.objID}`)] }, `ref-frag-${index}-${ref.objID}`));
1371
- })] }) }))] }), (showCommentForm && TID && DID) &&
1371
+ })] }) })] }), (showCommentForm && TID && DID) &&
1372
1372
  _jsx(TMBlogCommentForm, { context: { engine: 'SearchEngine', object: { tid: TID, did: DID } }, onClose: () => setShowCommentForm(false), refreshCallback: handleCompleteMoreInfo, participants: [], showAttachmentsSection: false, allArchivedDocumentsFileItems: [] }), isOpenDetails &&
1373
1373
  _jsx(StyledModalContainer, { children: _jsx(TMMasterDetailDcmts, { deviceType: deviceType, isForMaster: false, inputDcmts: getSelectionDcmtInfo(), allowNavigation: allowNavigation, canNext: canNext, canPrev: canPrev, onNext: onNext, onPrev: onPrev, onBack: () => setIsOpenDetails(false), allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers }) }), isOpenMaster &&
1374
1374
  _jsxs(StyledModalContainer, { children: [_jsx(TMMasterDetailDcmts, { deviceType: deviceType, inputDcmts: getSelectionDcmtInfo(), isForMaster: true, allowNavigation: allowNavigation, canNext: canNext, canPrev: canPrev, onNext: onNext, onPrev: onPrev, onBack: () => setIsOpenMaster(false), appendMasterDcmts: handleAddItem, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers }), secondaryMasterDcmts.length > 0 && secondaryMasterDcmts.map((dcmt, index) => {
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { PlatformObjectValidator, WhereItem, SDK_Localizator, OrderByItem, SelectItem, SelectItemVisibilities, SDK_Globals, SavedQueryCacheService, SearchEngine, QueryOperators } from '@topconsultnpm/sdk-ts';
4
4
  import styled from 'styled-components';
5
5
  import TMSearchQueryEditor from './TMSearchQueryEditor';
6
- import Toppy from '../../../assets/Toppy-generico.png';
7
6
  import { getDcmtTypesByQdAsync, SDKUI_Localizator, getQD, IconMenuVertical, IconAddCircleOutline, IconEdit, IconEasy, IconAdvanced, deepCompare, IconSearch, IconClear, getDefaultOperator, prepareQdForSearchAsync, IsParametricQuery, SDKUI_Globals, IconArrowRight, IconMenuCAArchive, getListMaxItems } from '../../../helper';
8
7
  import { useQueryParametersDialog } from '../../../hooks/useQueryParametersDialog';
9
8
  import { FormModes } from '../../../ts';
@@ -13,7 +12,6 @@ import ShowAlert from '../../base/TMAlert';
13
12
  import TMButton from '../../base/TMButton';
14
13
  import { useDeviceType, DeviceType } from '../../base/TMDeviceProvider';
15
14
  import TMDropDownMenu from '../../base/TMDropDownMenu';
16
- import TMLayoutContainer from '../../base/TMLayout';
17
15
  import { TMExceptionBoxManager } from '../../base/TMPopUp';
18
16
  import TMSpinner from '../../base/TMSpinner';
19
17
  import TMPanel from '../../base/TMPanel';
@@ -22,6 +20,7 @@ import { TMMetadataChooserForm } from '../../choosers/TMMetadataChooser';
22
20
  import TMQueryEditor from '../../query/TMQueryEditor';
23
21
  import TMSavedQueryForm from './TMSavedQueryForm';
24
22
  import { AdvancedMenuButtons } from '../../editors/TMMetadataValues';
23
+ import TMToppyMessage from '../../../helper/TMToppyMessage';
25
24
  const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SDKUI_Globals.userSettings.advancedSettings.expertMode === 1, SQD, inputMids, onSearchCompleted, onSqdSaved, onBack, onClosePanel, allowMaximize = true, onMaximizePanel, onBackToResult, passToArchiveCallback }) => {
26
25
  const [confirmQueryParams, ConfirmQueryParamsDialog] = useQueryParametersDialog();
27
26
  const [qd, setQd] = useState();
@@ -329,7 +328,7 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
329
328
  _jsx(TMMetadataChooserForm, { allowMultipleSelection: true, height: '500px', width: '600px', allowSysMetadata: true, qd: qd, selectedIDs: qd?.select?.map((item) => ({ tid: item.tid, mid: item.mid })), onClose: handleCloseOutputConfig, onChoose: handleChooseOutput }), showOrderByConfig &&
330
329
  _jsx(TMMetadataChooserForm, { allowMultipleSelection: true, height: '500px', width: '600px', allowSysMetadata: true, qd: qd, selectedIDs: qd?.orderBy?.map((item) => ({ tid: item.tid, mid: item.mid })), onClose: handleCloseOrderByConfig, onChoose: handleChooseOrderBy })] })
331
330
  :
332
- _jsxs(TMLayoutContainer, { gap: 30, alignItems: 'center', justifyContent: 'center', children: [_jsx(StyledToppyTextContainer, { children: _jsx(StyledToppyText, { title: SDKUI_Localizator.DcmtTypeSelectOrQuickSearch, children: SDKUI_Localizator.DcmtTypeSelectOrQuickSearch }) }), _jsx(StyledToppyImage, { src: Toppy, alt: 'Toppy' })] }), showSqdForm &&
331
+ _jsx(TMToppyMessage, { message: SDKUI_Localizator.DcmtTypeSelectOrQuickSearch, titleTooltip: SDKUI_Localizator.DcmtTypeSelectOrQuickSearch }), showSqdForm &&
333
332
  _jsx(StyledModalContainer, { style: { backgroundColor: `${TMColors.backgroundColorHeader}12` }, children: _jsx(TMSavedQueryForm, { height: '50%', width: '50%', id: formModeSqdForm === FormModes.Create ? -1 : SQD?.id, title: 'Ricerca rapida', formMode: formModeSqdForm, showBackButton: true, qd: qd, isAdvancedSearch: showAdvancedSearch, isModal: false, onClose: () => { setShowSqdForm(false); }, onSaved: onSqdSaved }) })] }), showDistinctValuesPanel &&
334
333
  _jsx(TMDistinctValues, { isModal: true, tid: focusedTidMid?.tid, mid: focusedTidMid?.mid, separator: ',', onClosePanelCallback: () => setShowDistinctValuesPanel(false), onSelectionChanged: (e) => {
335
334
  if (!e)
@@ -584,18 +584,17 @@ const TMSearchResult = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, a
584
584
  setIsOpenBatchUpdate(false);
585
585
  setIsModifiedBatchUpdate(false);
586
586
  await refreshSelectionDataRowsAsync();
587
- }, onStatusChanged: (isModified) => { setIsModifiedBatchUpdate(isModified); } }), (showToppyForApprove && !showApprovePopup && !showRejectPopup && !showReAssignPopup && !showMoreInfoPopup && !openS4TViewer && !showTodoDcmtForm) &&
588
- _jsx(ToppyDraggableHelpCenter, { initialIsCollapsed: false, deviceType: deviceType, content: _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '10px' }, children: _jsx(WorkFlowOperationButtons, { deviceType: deviceType, onApprove: () => {
589
- setShowApprovePopup(true);
590
- }, onSignApprove: () => {
591
- handleSignApprove();
592
- }, onReject: () => {
593
- setShowRejectPopup(true);
594
- }, onReAssign: () => {
595
- setShowReAssignPopup(true);
596
- }, onMoreInfo: () => {
597
- setShowMoreInfoPopup(true);
598
- }, approveDisable: selectedDocs.length === 0, signApproveDisable: disableSignApproveDisable, rejectDisable: selectedDocs.length === 0, reassignDisable: selectedDocs.length === 0, infoDisable: selectedDocs.length !== 1, dtd: fromDTD }) }) })] }), _jsx(ConfirmFormatDialog, {}), _jsx(ConfirmAttachmentsDialog, {}), customButton && _jsx(TMCustomButton, { button: customButton, formData: currentMetadataValues, selectedItems: selectedItems, onClose: () => setCustomButton(undefined) }), showRelatedDcmtsChooser &&
587
+ }, onStatusChanged: (isModified) => { setIsModifiedBatchUpdate(isModified); } }), _jsx(ToppyDraggableHelpCenter, { initialIsCollapsed: false, deviceType: deviceType, usePortal: true, isVisible: showToppyForApprove && !showApprovePopup && !showRejectPopup && !showReAssignPopup && !showMoreInfoPopup && !openS4TViewer && !showTodoDcmtForm, content: _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '10px' }, children: _jsx(WorkFlowOperationButtons, { deviceType: deviceType, onApprove: () => {
588
+ setShowApprovePopup(true);
589
+ }, onSignApprove: () => {
590
+ handleSignApprove();
591
+ }, onReject: () => {
592
+ setShowRejectPopup(true);
593
+ }, onReAssign: () => {
594
+ setShowReAssignPopup(true);
595
+ }, onMoreInfo: () => {
596
+ setShowMoreInfoPopup(true);
597
+ }, approveDisable: selectedDocs.length === 0, signApproveDisable: disableSignApproveDisable, rejectDisable: selectedDocs.length === 0, reassignDisable: selectedDocs.length === 0, infoDisable: selectedDocs.length !== 1, dtd: fromDTD }) }) })] }), _jsx(ConfirmFormatDialog, {}), _jsx(ConfirmAttachmentsDialog, {}), customButton && _jsx(TMCustomButton, { button: customButton, formData: currentMetadataValues, selectedItems: selectedItems, onClose: () => setCustomButton(undefined) }), showRelatedDcmtsChooser &&
599
598
  _jsx(TMChooserForm, { dataSource: relatedDcmtsChooserDataSource, onChoose: async (selectedRelation) => {
600
599
  try {
601
600
  setShowRelatedDcmtsChooser(false);
@@ -59,7 +59,6 @@ export { default as TMBlogAttachments } from './grids/TMBlogAttachments';
59
59
  export { default as TMBlogCommentForm } from './features/blog/TMBlogCommentForm';
60
60
  export * from './query/TMQueryEditor';
61
61
  export * from './query/TMQuerySummary';
62
- export { default as ToppyHelpCenter } from './features/assistant/ToppyHelpCenter';
63
62
  export { default as ToppyDraggableHelpCenter } from './features/assistant/ToppyDraggableHelpCenter';
64
63
  export * from './features/documents/TMDcmtForm';
65
64
  export * from './features/documents/TMDcmtIcon';
@@ -66,7 +66,6 @@ export { default as TMBlogCommentForm } from './features/blog/TMBlogCommentForm'
66
66
  export * from './query/TMQueryEditor';
67
67
  export * from './query/TMQuerySummary';
68
68
  //assistant
69
- export { default as ToppyHelpCenter } from './features/assistant/ToppyHelpCenter';
70
69
  export { default as ToppyDraggableHelpCenter } from './features/assistant/ToppyDraggableHelpCenter';
71
70
  //documents
72
71
  export * from './features/documents/TMDcmtForm';
@@ -34,9 +34,10 @@ const StyledToppyImage = styled.img `
34
34
  max-width: 120px;
35
35
  height: auto;
36
36
  display: block;
37
+ user-select: none;
37
38
  `;
38
39
  const TMToppyMessage = (props) => {
39
40
  const { message, titleTooltip } = props;
40
- return (_jsxs(TMLayoutContainer, { gap: 30, alignItems: "center", justifyContent: "center", onContextMenu: (e) => e.preventDefault(), children: [_jsx(StyledToppyTextContainer, { children: _jsx(StyledToppyText, { title: titleTooltip || undefined, children: message }) }), _jsx(StyledToppyImage, { src: Toppy, alt: "Toppy" })] }));
41
+ return (_jsxs(TMLayoutContainer, { gap: 30, alignItems: "center", justifyContent: "center", onContextMenu: (e) => e.preventDefault(), children: [_jsx(StyledToppyTextContainer, { children: _jsx(StyledToppyText, { title: titleTooltip || undefined, children: message }) }), _jsx(StyledToppyImage, { src: Toppy, alt: "Toppy", draggable: false, onMouseDown: (e) => e.stopPropagation() })] }));
41
42
  };
42
43
  export default TMToppyMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.19.0-dev2.52",
3
+ "version": "6.19.0-dev2.54",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -1,12 +0,0 @@
1
- import React from 'react';
2
- import { DeviceType } from '../../base/TMDeviceProvider';
3
- interface ToppyHelpCenterProps {
4
- content?: React.ReactNode;
5
- deviceType?: DeviceType;
6
- usePortal?: boolean;
7
- align?: 'right' | 'left';
8
- initialIsCollapsed?: boolean;
9
- onToppyImageClick?: () => void;
10
- }
11
- declare const ToppyHelpCenter: React.FC<ToppyHelpCenterProps>;
12
- export default ToppyHelpCenter;
@@ -1,173 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import ReactDOM from 'react-dom';
4
- import styled, { keyframes, css } from 'styled-components';
5
- import Toppy from '../../../assets/Toppy-generico.png';
6
- import { DeviceType } from '../../base/TMDeviceProvider';
7
- const toppyEntrance = keyframes `
8
- 0% { right: -200px; opacity: 0; }
9
- 60% { opacity: 1; }
10
- 100% { right: 10px; opacity: 1; }
11
- `;
12
- const pulseAnimation = keyframes `
13
- 0% { transform: scale(1); }
14
- 50% { transform: scale(1.05); }
15
- 100% { transform: scale(1); }
16
- `;
17
- const bounceAnimation = keyframes `
18
- 0%, 20%, 50%, 80%, 100% {
19
- transform: translateY(0);
20
- }
21
- 40% {
22
- transform: translateY(-10px);
23
- }
24
- 60% {
25
- transform: translateY(-5px);
26
- }
27
- `;
28
- const wiggle = keyframes `
29
- 0% { transform: rotate(-5deg) scale(1.1); }
30
- 10% { transform: rotate(5deg) scale(0.95); }
31
- 20% { transform: rotate(-5deg) scale(1.1); }
32
- 30% { transform: rotate(5deg) scale(1.05); }
33
- 40% { transform: rotate(-5deg) scale(1); }
34
- 50% { transform: rotate(5deg) scale(0.96); }
35
- 60% { transform: rotate(-5deg) scale(1.15); }
36
- 70% { transform: rotate(5deg) scale(0.99); }
37
- 80% { transform: rotate(-5deg) scale(1.05); }
38
- 90% { transform: rotate(5deg) scale(1); }
39
- 100% { transform: rotate(-5deg) scale(1.08); }
40
- `;
41
- const ToppyContainer = styled.div `
42
- position: ${({ $fixed }) => ($fixed ? 'fixed' : 'absolute')};
43
- bottom: ${({ $isCollapsed, $isMobile }) => $isMobile ? '5px' : $isCollapsed ? '5px' : '-20px'};
44
- ${({ $align, $isCollapsed }) => $align === 'left'
45
- ? `left: ${$isCollapsed ? '5px' : '10px'}; right: auto;`
46
- : `right: ${$isCollapsed ? '5px' : '10px'}; left: auto;`}
47
- display: flex;
48
- flex-direction: column-reverse; /* Il contenuto è sopra l'immagine */
49
- align-items: ${({ $align }) => $align === 'left' ? 'flex-start' : 'flex-end'};
50
- animation: ${({ $fixed }) => $fixed && css `${toppyEntrance} 0.5s cubic-bezier(0.23, 1, 0.32, 1)`};
51
- z-index: ${({ $fixed }) => ($fixed ? 2147483647 : 10)};
52
- `;
53
- const ToppyImage = styled.img `
54
- width: ${({ $isMobile, $isCollapsed }) => $isCollapsed ? '60px' : $isMobile ? '90px' : '120px'};
55
- height: ${({ $isMobile, $isCollapsed }) => $isCollapsed ? '70px' : $isMobile ? '105px' : '140px'};
56
- cursor: pointer;
57
- object-fit: cover;
58
- object-position: top center;
59
- clip-path: inset(0 0 22% 0 round 10px);
60
- transform: ${({ $isCollapsed, $align }) => $isCollapsed
61
- ? 'none'
62
- : $align === 'left'
63
- ? 'rotate(20deg)'
64
- : 'rotate(-20deg)'};
65
-
66
- ${({ $isCollapsed }) => $isCollapsed &&
67
- css `
68
- /* animation: ${bounceAnimation} 2s infinite; */
69
- /* animation: ${pulseAnimation} 2s infinite; */
70
- animation: ${wiggle} 4s infinite;
71
- `}
72
- `;
73
- const ToppyContent = styled.div `
74
- margin-bottom: ${({ $isMobile, $align }) => $align === 'left'
75
- ? '30px' // Spazio tra ToppyContent e ToppyImage
76
- : $isMobile
77
- ? '30px'
78
- : '20px'};
79
-
80
-
81
- display: ${props => (props.$isCollapsed ? 'none' : 'block')};
82
- width: max-content;
83
- max-width: 250px;
84
- background: linear-gradient(
85
- 180deg,
86
- rgba(0, 113, 188, 0.45) 0%,
87
- rgba(27, 20, 100, 0.65) 100%
88
- );
89
- color: white;
90
- padding: 10px;
91
- border-radius: 10px;
92
- border: 1px solid #FFFFFF;
93
- opacity: ${props => (props.$isCollapsed ? 0 : 1)};
94
- transform: ${props => (props.$isCollapsed ? 'translateY(20px)' : 'translateY(0)')};
95
- transition: opacity 0.3s ease, transform 0.3s ease;
96
- pointer-events: ${props => (props.$isCollapsed ? 'none' : 'auto')};
97
- position: relative;
98
-
99
- ${({ $align, $isMobile }) => $align === 'left' &&
100
- css `
101
- position: absolute;
102
- left: 50px;
103
- right: auto;
104
- bottom: 100%;
105
- transform: translateY(-${$isMobile ? '10px' : '20px'});
106
- `}
107
-
108
- &::after {
109
- transform: ${({ $align }) => $align === 'left' ? 'skewX(15deg)' : 'skewX(-15deg)'};
110
- content: "";
111
- position: absolute;
112
- top: 100%;
113
-
114
- ${({ $align }) => $align === 'left' ? 'left: 20px; right: auto;' : 'right: 15px; left: auto;'}
115
- border-width: 32px 32px 0 0;
116
- border-style: solid;
117
- border-color: #FFFFFF transparent;
118
- display: block;
119
- width: 0;
120
- height: 0;
121
- z-index: 1;
122
- }
123
-
124
- &::before {
125
- transform: ${({ $align }) => $align === 'left' ? 'skewX(15deg)' : 'skewX(-15deg)'};
126
- content: "";
127
- position: absolute;
128
- top: 100%;
129
-
130
- ${({ $align }) => $align === 'left' ? 'left: 20px; right: auto;' : 'right: 15px; left: auto;'}
131
- border-width: 32px 32px 0 0;
132
- border-style: solid;
133
- border-color: rgba(27, 20, 100, 0.65) transparent;
134
- display: block;
135
- width: 0;
136
- height: 0;
137
- z-index: 2;
138
- }
139
- `;
140
- const ToppyHelpCenter = ({ content, deviceType, usePortal = true, align = 'right', initialIsCollapsed, onToppyImageClick }) => {
141
- const [isCollapsed, setIsCollapsed] = useState(initialIsCollapsed ?? false);
142
- const [portalContainer, setPortalContainer] = useState(null);
143
- useEffect(() => {
144
- if (initialIsCollapsed === undefined && deviceType === DeviceType.MOBILE) {
145
- setIsCollapsed(true);
146
- }
147
- }, [deviceType, initialIsCollapsed]);
148
- useEffect(() => {
149
- if (!usePortal)
150
- return;
151
- const portalRoot = document.createElement('div');
152
- portalRoot.setAttribute('id', 'toppy-portal-root');
153
- document.body.appendChild(portalRoot);
154
- setPortalContainer(portalRoot);
155
- return () => {
156
- document.body.removeChild(portalRoot);
157
- };
158
- }, [usePortal]);
159
- const toggleCollapse = (e) => {
160
- e.stopPropagation();
161
- setIsCollapsed(!isCollapsed);
162
- onToppyImageClick?.();
163
- };
164
- const isMobile = deviceType === DeviceType.MOBILE;
165
- const toppyComponent = (_jsxs(ToppyContainer, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$fixed": usePortal, "$align": align, onContextMenu: (e) => e.preventDefault(), children: [_jsx(ToppyImage, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$align": align, onClick: toggleCollapse, src: Toppy, alt: "Toppy" }), _jsx(ToppyContent, { "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$align": align, children: content })] }));
166
- if (usePortal) {
167
- if (!portalContainer)
168
- return null;
169
- return ReactDOM.createPortal(toppyComponent, portalContainer);
170
- }
171
- return toppyComponent;
172
- };
173
- export default ToppyHelpCenter;