@magicfeedback/popup-sdk 1.8.5 → 1.9.0

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.
package/dist/index.d.mts CHANGED
@@ -192,6 +192,12 @@ interface FeedbackSinkOptions {
192
192
  onSessionId?: (id: string) => void;
193
193
  /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */
194
194
  onSessionReset?: () => void;
195
+ /**
196
+ * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del
197
+ * registro cerrado: el que llevaba el lote o, en una sesión de un solo lote, el que
198
+ * devuelve la respuesta. Ese registro se convierte en Feedback en segundos.
199
+ */
200
+ onSessionClosed?: (id: string) => void;
195
201
  }
196
202
  /**
197
203
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
@@ -226,6 +232,19 @@ interface DeepdotsLogger {
226
232
  error?: (...args: unknown[]) => void;
227
233
  info?: (...args: unknown[]) => void;
228
234
  }
235
+ /**
236
+ * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE
237
+ * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look
238
+ * that feedback up with (`GET /feedbacks?filter={"where":{"sdkSessionId":"<sessionId>"}}`).
239
+ * A session that closes again keeps that one feedback: the API updates it.
240
+ * - `open`: the API acknowledged the session's first batch; its events keep accumulating.
241
+ * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the
242
+ * feedback is created from it within seconds.
243
+ */
244
+ interface FeedbackSession {
245
+ sessionId: string;
246
+ status: 'open' | 'closed';
247
+ }
229
248
  interface DeepdotsInitParams {
230
249
  /** API key for authentication */
231
250
  apiKey?: string;
@@ -261,6 +280,17 @@ interface DeepdotsInitParams {
261
280
  * logged when `debug` is true.
262
281
  */
263
282
  analytics?: AnalyticsKeys;
283
+ /**
284
+ * Called when the analytics channel opens or closes a session (needs `analytics`), with the
285
+ * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it
286
+ * to your backend to attach data to that feedback later. A session whose only batch is the
287
+ * closing one reports just `closed`. `closed` does not come when the page unloads before the
288
+ * closing request returns, for a session dropped after 30 min hidden, nor when the closing
289
+ * request fails (the API closes those itself, later): their id already came with `open`. Key
290
+ * by `sessionId`, not by order: the next session's `open` can arrive before the previous
291
+ * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.
292
+ */
293
+ onFeedbackSession?: (session: FeedbackSession) => void;
264
294
  /**
265
295
  * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
266
296
  * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
@@ -698,6 +728,8 @@ declare class DeepdotsPopups {
698
728
  private analytics;
699
729
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
700
730
  private analyticsFeedbackSessionId;
731
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
732
+ private onFeedbackSession;
701
733
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
702
734
  private analyticsFlushTimer;
703
735
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -741,6 +773,15 @@ declare class DeepdotsPopups {
741
773
  getUserId(): string | null;
742
774
  /** Session id de navegación actual. Null si tracking off. */
743
775
  getSessionId(): string | null;
776
+ /**
777
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
778
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
779
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
780
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
781
+ */
782
+ getFeedbackSessionId(): string | null;
783
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
784
+ private notifyFeedbackSession;
744
785
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
745
786
  setTrackingEnabled(enabled: boolean): void;
746
787
  /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
@@ -1265,4 +1306,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
1265
1306
  */
1266
1307
  declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
1267
1308
 
1268
- export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type BuildBodyOptions, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, type DeepdotsInitParams, type DeepdotsLogger, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, LABELS, MAX_TRACKED_MESSAGES, MESSAGE_CHANNELS, type MessageChannel, MessageGuard, type MessageGuardVerdict, type MessageRejectionReason, type MessageStage, type MmkvLike, NATIVE_READY_GRACE_MS, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupLabels, type PopupLocale, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, REVEAL_TIMEOUT_MS, RTL_LOCALES, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type ResolvedActionLabels, SUPPORTED_LANGUAGES, type SessionEndReason, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, directionFor, getLabels, isRtlLanguage, mmkvStorage, resolveActionLabels, resolveLocale, setupReactNative };
1309
+ export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type BuildBodyOptions, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, type DeepdotsInitParams, type DeepdotsLogger, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, type FeedbackSession, InMemoryStorage, type KeyValueStorage, LABELS, MAX_TRACKED_MESSAGES, MESSAGE_CHANNELS, type MessageChannel, MessageGuard, type MessageGuardVerdict, type MessageRejectionReason, type MessageStage, type MmkvLike, NATIVE_READY_GRACE_MS, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupLabels, type PopupLocale, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, REVEAL_TIMEOUT_MS, RTL_LOCALES, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type ResolvedActionLabels, SUPPORTED_LANGUAGES, type SessionEndReason, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, directionFor, getLabels, isRtlLanguage, mmkvStorage, resolveActionLabels, resolveLocale, setupReactNative };
package/dist/index.d.ts CHANGED
@@ -192,6 +192,12 @@ interface FeedbackSinkOptions {
192
192
  onSessionId?: (id: string) => void;
193
193
  /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */
194
194
  onSessionReset?: () => void;
195
+ /**
196
+ * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del
197
+ * registro cerrado: el que llevaba el lote o, en una sesión de un solo lote, el que
198
+ * devuelve la respuesta. Ese registro se convierte en Feedback en segundos.
199
+ */
200
+ onSessionClosed?: (id: string) => void;
195
201
  }
196
202
  /**
197
203
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
@@ -226,6 +232,19 @@ interface DeepdotsLogger {
226
232
  error?: (...args: unknown[]) => void;
227
233
  info?: (...args: unknown[]) => void;
228
234
  }
235
+ /**
236
+ * A session of the analytics channel as the Deepdots API sees it. Every session becomes ONE
237
+ * feedback, and `sessionId` is what the API stores as `sdkSessionId` on it: the value to look
238
+ * that feedback up with (`GET /feedbacks?filter={"where":{"sdkSessionId":"<sessionId>"}}`).
239
+ * A session that closes again keeps that one feedback: the API updates it.
240
+ * - `open`: the API acknowledged the session's first batch; its events keep accumulating.
241
+ * - `closed`: the API acknowledged the session's closing batch (`completed: true`); the
242
+ * feedback is created from it within seconds.
243
+ */
244
+ interface FeedbackSession {
245
+ sessionId: string;
246
+ status: 'open' | 'closed';
247
+ }
229
248
  interface DeepdotsInitParams {
230
249
  /** API key for authentication */
231
250
  apiKey?: string;
@@ -261,6 +280,17 @@ interface DeepdotsInitParams {
261
280
  * logged when `debug` is true.
262
281
  */
263
282
  analytics?: AnalyticsKeys;
283
+ /**
284
+ * Called when the analytics channel opens or closes a session (needs `analytics`), with the
285
+ * id the Deepdots API stores as `sdkSessionId` on the feedback the session becomes: send it
286
+ * to your backend to attach data to that feedback later. A session whose only batch is the
287
+ * closing one reports just `closed`. `closed` does not come when the page unloads before the
288
+ * closing request returns, for a session dropped after 30 min hidden, nor when the closing
289
+ * request fails (the API closes those itself, later): their id already came with `open`. Key
290
+ * by `sessionId`, not by order: the next session's `open` can arrive before the previous
291
+ * one's `closed`. Same value as `getFeedbackSessionId()` while the session is open.
292
+ */
293
+ onFeedbackSession?: (session: FeedbackSession) => void;
264
294
  /**
265
295
  * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
266
296
  * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
@@ -698,6 +728,8 @@ declare class DeepdotsPopups {
698
728
  private analytics;
699
729
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
700
730
  private analyticsFeedbackSessionId;
731
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
732
+ private onFeedbackSession;
701
733
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
702
734
  private analyticsFlushTimer;
703
735
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -741,6 +773,15 @@ declare class DeepdotsPopups {
741
773
  getUserId(): string | null;
742
774
  /** Session id de navegación actual. Null si tracking off. */
743
775
  getSessionId(): string | null;
776
+ /**
777
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
778
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
779
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
780
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
781
+ */
782
+ getFeedbackSessionId(): string | null;
783
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
784
+ private notifyFeedbackSession;
744
785
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
745
786
  setTrackingEnabled(enabled: boolean): void;
746
787
  /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
@@ -1265,4 +1306,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
1265
1306
  */
1266
1307
  declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
1267
1308
 
1268
- export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type BuildBodyOptions, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, type DeepdotsInitParams, type DeepdotsLogger, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, LABELS, MAX_TRACKED_MESSAGES, MESSAGE_CHANNELS, type MessageChannel, MessageGuard, type MessageGuardVerdict, type MessageRejectionReason, type MessageStage, type MmkvLike, NATIVE_READY_GRACE_MS, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupLabels, type PopupLocale, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, REVEAL_TIMEOUT_MS, RTL_LOCALES, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type ResolvedActionLabels, SUPPORTED_LANGUAGES, type SessionEndReason, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, directionFor, getLabels, isRtlLanguage, mmkvStorage, resolveActionLabels, resolveLocale, setupReactNative };
1309
+ export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type BuildBodyOptions, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, type DeepdotsInitParams, type DeepdotsLogger, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, type FeedbackSession, InMemoryStorage, type KeyValueStorage, LABELS, MAX_TRACKED_MESSAGES, MESSAGE_CHANNELS, type MessageChannel, MessageGuard, type MessageGuardVerdict, type MessageRejectionReason, type MessageStage, type MmkvLike, NATIVE_READY_GRACE_MS, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupLabels, type PopupLocale, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, REVEAL_TIMEOUT_MS, RTL_LOCALES, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type ResolvedActionLabels, SUPPORTED_LANGUAGES, type SessionEndReason, type TrackMessageOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildMessageParams, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, directionFor, getLabels, isRtlLanguage, mmkvStorage, resolveActionLabels, resolveLocale, setupReactNative };
package/dist/index.js CHANGED
@@ -583,7 +583,9 @@ var init_style = __esm({
583
583
  * repintar\xEDa su web.
584
584
  * 4. El grupo .magicfeedback-radio se cualifica como div.magicfeedback-radio (la clase acaba
585
585
  * tambi\xE9n en el <input> del boolean).
586
- * 5. Al final, un bloque de reglas propias del popup (fuente en los controles y padding del
586
+ * 5. Todo :hover va dentro de @media (hover: hover): en t\xE1ctil WebKit deja el hover pegado
587
+ * en el \xFAltimo punto tocado y la opci\xF3n de la p\xE1gina siguiente parec\xEDa seleccionada.
588
+ * 6. Al final, un bloque de reglas propias del popup (fuente en los controles y padding del
587
589
  * radio nativo).
588
590
  *
589
591
  * Las variables --mf-* se dejan en :root a prop\xF3sito: el modal del priority-list se portea a
@@ -821,9 +823,11 @@ var init_style = __esm({
821
823
  text-underline-offset: 2px;
822
824
  }
823
825
 
824
- .magicfeedback-info-message a:hover,
825
- .magicfeedback-start-message a:hover {
826
- color: var(--mf-primary-hover);
826
+ @media (hover: hover) {
827
+ .magicfeedback-info-message a:hover,
828
+ .magicfeedback-start-message a:hover {
829
+ color: var(--mf-primary-hover);
830
+ }
827
831
  }
828
832
 
829
833
  .magicfeedback-info-message ul,
@@ -1049,10 +1053,12 @@ div.magicfeedback-radio {
1049
1053
  transition: var(--mf-transition-fast);
1050
1054
  }
1051
1055
 
1052
- .magicfeedback-checkbox-container:hover,
1053
- .magicfeedback-radio-container:hover {
1054
- background-color: var(--mf-bg-hover);
1055
- border-color: var(--mf-primary);
1056
+ @media (hover: hover) {
1057
+ .magicfeedback-checkbox-container:hover,
1058
+ .magicfeedback-radio-container:hover {
1059
+ background-color: var(--mf-bg-hover);
1060
+ border-color: var(--mf-primary);
1061
+ }
1056
1062
  }
1057
1063
 
1058
1064
  .magicfeedback-checkbox-container:has(input:checked),
@@ -1119,9 +1125,11 @@ div.magicfeedback-radio {
1119
1125
  height: inherit;
1120
1126
  }
1121
1127
 
1122
- .magicfeedback-boolean-option label:hover {
1123
- background-color: var(--mf-bg-hover);
1124
- border-color: var(--mf-primary);
1128
+ @media (hover: hover) {
1129
+ .magicfeedback-boolean-option label:hover {
1130
+ background-color: var(--mf-bg-hover);
1131
+ border-color: var(--mf-primary);
1132
+ }
1125
1133
  }
1126
1134
 
1127
1135
  .magicfeedback-boolean-option input[type="radio"]:checked + label {
@@ -1192,9 +1200,11 @@ div.magicfeedback-radio {
1192
1200
  border: 2px solid transparent;
1193
1201
  }
1194
1202
 
1195
- .magicfeedback-rating-option-label-container img:hover {
1196
- transform: scale(1.1);
1197
- border-color: var(--mf-primary);
1203
+ @media (hover: hover) {
1204
+ .magicfeedback-rating-option-label-container img:hover {
1205
+ transform: scale(1.1);
1206
+ border-color: var(--mf-primary);
1207
+ }
1198
1208
  }
1199
1209
 
1200
1210
  .magicfeedback-rating-option-label-container input[type="radio"]:checked + img {
@@ -1346,9 +1356,11 @@ div.magicfeedback-radio {
1346
1356
  font-size: 0.8rem;
1347
1357
  }
1348
1358
 
1349
- .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1350
- .magicfeedback-rating-number-option-row-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1351
- background: var(--mf-primary-light);
1359
+ @media (hover: hover) {
1360
+ .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1361
+ .magicfeedback-rating-number-option-row-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1362
+ background: var(--mf-primary-light);
1363
+ }
1352
1364
  }
1353
1365
 
1354
1366
  .magicfeedback-rating-number-option-row .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value,
@@ -1385,8 +1397,10 @@ div.magicfeedback-radio {
1385
1397
  font-size: var(--mf-font-size-sm);
1386
1398
  }
1387
1399
 
1388
- .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1389
- background: var(--mf-primary-light);
1400
+ @media (hover: hover) {
1401
+ .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1402
+ background: var(--mf-primary-light);
1403
+ }
1390
1404
  }
1391
1405
 
1392
1406
  .magicfeedback-rating-number-option--extra .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value {
@@ -1419,9 +1433,11 @@ div.magicfeedback-radio {
1419
1433
  color: var(--mf-primary);
1420
1434
  }
1421
1435
 
1422
- .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1423
- .magicfeedback-rating-number-option-column-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1424
- background: var(--mf-primary-light);
1436
+ @media (hover: hover) {
1437
+ .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value,
1438
+ .magicfeedback-rating-number-option-column-reverse .magicfeedback-rating-number-option-label-container:hover .magicfeedback-rating-number-value {
1439
+ background: var(--mf-primary-light);
1440
+ }
1425
1441
  }
1426
1442
 
1427
1443
  .magicfeedback-rating-number-option-column .magicfeedback-rating-number-option-label-container input[type="radio"]:checked + .magicfeedback-rating-number-value,
@@ -1475,10 +1491,12 @@ div.magicfeedback-radio {
1475
1491
  transition: var(--mf-transition-fast);
1476
1492
  }
1477
1493
 
1478
- .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover,
1479
- .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover ~ span {
1480
- color: #fbbf24;
1481
- transform: scale(1.1);
1494
+ @media (hover: hover) {
1495
+ .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover,
1496
+ .magicfeedback-rating-star-option:not(:checked) > .rating__star:hover ~ span {
1497
+ color: #fbbf24;
1498
+ transform: scale(1.1);
1499
+ }
1482
1500
  }
1483
1501
 
1484
1502
  .magicfeedback-rating-star-selected {
@@ -1519,9 +1537,11 @@ div.magicfeedback-radio {
1519
1537
  transition: var(--mf-transition-fast);
1520
1538
  }
1521
1539
 
1522
- .magicfeedback-image-option-label-container:hover img {
1523
- border-color: var(--mf-primary);
1524
- box-shadow: var(--mf-shadow-md);
1540
+ @media (hover: hover) {
1541
+ .magicfeedback-image-option-label-container:hover img {
1542
+ border-color: var(--mf-primary);
1543
+ box-shadow: var(--mf-shadow-md);
1544
+ }
1525
1545
  }
1526
1546
 
1527
1547
  .magicfeedback-image-option-label-container input:checked + img {
@@ -1562,9 +1582,11 @@ div.magicfeedback-radio {
1562
1582
  background-color: var(--mf-bg-secondary);
1563
1583
  }
1564
1584
 
1565
- .magicfeedback-multi-question-matrix-row-tr:hover {
1566
- background-color: var(--mf-bg-hover);
1567
- box-shadow: var(--mf-shadow-sm);
1585
+ @media (hover: hover) {
1586
+ .magicfeedback-multi-question-matrix-row-tr:hover {
1587
+ background-color: var(--mf-bg-hover);
1588
+ box-shadow: var(--mf-shadow-sm);
1589
+ }
1568
1590
  }
1569
1591
 
1570
1592
  .magicfeedback-multi-question-matrix-table td {
@@ -1600,9 +1622,11 @@ div.magicfeedback-radio {
1600
1622
  transition: var(--mf-transition-fast);
1601
1623
  }
1602
1624
 
1603
- .magicfeedback-priority-list-item:hover {
1604
- box-shadow: var(--mf-shadow-sm);
1605
- border-color: var(--mf-primary);
1625
+ @media (hover: hover) {
1626
+ .magicfeedback-priority-list-item:hover {
1627
+ box-shadow: var(--mf-shadow-sm);
1628
+ border-color: var(--mf-primary);
1629
+ }
1606
1630
  }
1607
1631
 
1608
1632
  .magicfeedback-priority-list-item-label {
@@ -1623,10 +1647,12 @@ div.magicfeedback-radio {
1623
1647
  user-select: none;
1624
1648
  }
1625
1649
 
1626
- .magicfeedback-priority-list-arrow-up:hover,
1627
- .magicfeedback-priority-list-arrow-down:hover {
1628
- color: var(--mf-primary);
1629
- transform: scale(1.2);
1650
+ @media (hover: hover) {
1651
+ .magicfeedback-priority-list-arrow-up:hover,
1652
+ .magicfeedback-priority-list-arrow-down:hover {
1653
+ color: var(--mf-primary);
1654
+ transform: scale(1.2);
1655
+ }
1630
1656
  }
1631
1657
 
1632
1658
  /* ========================================
@@ -1733,10 +1759,12 @@ div.magicfeedback-radio {
1733
1759
  box-shadow: var(--mf-shadow-md);
1734
1760
  }
1735
1761
 
1736
- .magicfeedback-submit:hover {
1737
- background-color: var(--mf-primary-hover);
1738
- box-shadow: var(--mf-shadow-lg);
1739
- transform: translateY(-2px);
1762
+ @media (hover: hover) {
1763
+ .magicfeedback-submit:hover {
1764
+ background-color: var(--mf-primary-hover);
1765
+ box-shadow: var(--mf-shadow-lg);
1766
+ transform: translateY(-2px);
1767
+ }
1740
1768
  }
1741
1769
 
1742
1770
  .magicfeedback-submit:active {
@@ -1762,9 +1790,11 @@ div.magicfeedback-radio {
1762
1790
  transition: var(--mf-transition-fast);
1763
1791
  }
1764
1792
 
1765
- .magicfeedback-back:hover {
1766
- border-color: var(--mf-primary);
1767
- background-color: var(--mf-bg-hover);
1793
+ @media (hover: hover) {
1794
+ .magicfeedback-back:hover {
1795
+ border-color: var(--mf-primary);
1796
+ background-color: var(--mf-bg-hover);
1797
+ }
1768
1798
  }
1769
1799
 
1770
1800
  /* ========================================
@@ -1784,8 +1814,10 @@ div.magicfeedback-radio {
1784
1814
  transition: var(--mf-transition-fast);
1785
1815
  }
1786
1816
 
1787
- .magicfeedback-skip-container:hover {
1788
- color: var(--mf-primary);
1817
+ @media (hover: hover) {
1818
+ .magicfeedback-skip-container:hover {
1819
+ color: var(--mf-primary);
1820
+ }
1789
1821
  }
1790
1822
 
1791
1823
  .magicfeedback-skip-container input[type="checkbox"] {
@@ -1926,9 +1958,11 @@ div.magicfeedback-radio {
1926
1958
  box-shadow: var(--mf-shadow-sm);
1927
1959
  }
1928
1960
 
1929
- .magicfeedback-button:hover {
1930
- background: var(--mf-bg-hover);
1931
- border-color: var(--mf-primary);
1961
+ @media (hover: hover) {
1962
+ .magicfeedback-button:hover {
1963
+ background: var(--mf-bg-hover);
1964
+ border-color: var(--mf-primary);
1965
+ }
1932
1966
  }
1933
1967
 
1934
1968
  .magicfeedback-button:active {
@@ -1942,8 +1976,10 @@ div.magicfeedback-radio {
1942
1976
  box-shadow: var(--mf-shadow-md);
1943
1977
  }
1944
1978
 
1945
- .magicfeedback-button-primary:hover {
1946
- background: var(--mf-primary-hover);
1979
+ @media (hover: hover) {
1980
+ .magicfeedback-button-primary:hover {
1981
+ background: var(--mf-primary-hover);
1982
+ }
1947
1983
  }
1948
1984
 
1949
1985
  .magicfeedback-button-primary:active {
@@ -1969,8 +2005,10 @@ div.magicfeedback-radio {
1969
2005
  box-shadow: var(--mf-shadow-md);
1970
2006
  }
1971
2007
 
1972
- .magicfeedback-start-message-button:hover {
1973
- background: var(--mf-primary-hover);
2008
+ @media (hover: hover) {
2009
+ .magicfeedback-start-message-button:hover {
2010
+ background: var(--mf-primary-hover);
2011
+ }
1974
2012
  }
1975
2013
 
1976
2014
  /* ========================================
@@ -2106,12 +2144,18 @@ div.magicfeedback-radio {
2106
2144
  color: #666;
2107
2145
  }
2108
2146
 
2109
- .magicfeedback-modal-close:hover,
2110
2147
  .magicfeedback-modal-close:focus {
2111
2148
  color: #000;
2112
2149
  outline: none;
2113
2150
  }
2114
2151
 
2152
+ @media (hover: hover) {
2153
+ .magicfeedback-modal-close:hover {
2154
+ color: #000;
2155
+ outline: none;
2156
+ }
2157
+ }
2158
+
2115
2159
  /* ========================================
2116
2160
  FILE / IMAGE UPLOAD
2117
2161
  ======================================== */
@@ -2155,9 +2199,11 @@ div.magicfeedback-radio {
2155
2199
  transition: var(--mf-transition);
2156
2200
  }
2157
2201
 
2158
- .magicfeedback-upload-dropzone:hover {
2159
- border-color: var(--mf-primary);
2160
- background-color: var(--mf-bg-hover);
2202
+ @media (hover: hover) {
2203
+ .magicfeedback-upload-dropzone:hover {
2204
+ border-color: var(--mf-primary);
2205
+ background-color: var(--mf-bg-hover);
2206
+ }
2161
2207
  }
2162
2208
 
2163
2209
  .magicfeedback-upload-dropzone:focus-within {
@@ -2295,13 +2341,20 @@ div.magicfeedback-radio {
2295
2341
  transition: var(--mf-transition-fast);
2296
2342
  }
2297
2343
 
2298
- .magicfeedback-upload-remove:hover,
2299
2344
  .magicfeedback-upload-remove:focus-visible {
2300
2345
  color: var(--mf-error);
2301
2346
  background-color: var(--mf-primary-light);
2302
2347
  outline: none;
2303
2348
  }
2304
2349
 
2350
+ @media (hover: hover) {
2351
+ .magicfeedback-upload-remove:hover {
2352
+ color: var(--mf-error);
2353
+ background-color: var(--mf-primary-light);
2354
+ outline: none;
2355
+ }
2356
+ }
2357
+
2305
2358
  .magicfeedback-upload-remove svg {
2306
2359
  width: 16px;
2307
2360
  height: 16px;
@@ -6816,8 +6869,10 @@ async function safeBodyText(res) {
6816
6869
  function createFeedbackSink(options) {
6817
6870
  let feedbackSessionId;
6818
6871
  let firstPostInFlight = null;
6872
+ let generation = 0;
6819
6873
  const url = `${options.baseUrl}/sdk/feedback`;
6820
6874
  const post = async (body, final, sessionEnd = false) => {
6875
+ const postGeneration = generation;
6821
6876
  const json = JSON.stringify(body);
6822
6877
  const small = json.length <= KEEPALIVE_MAX_BYTES;
6823
6878
  if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
@@ -6847,9 +6902,25 @@ function createFeedbackSink(options) {
6847
6902
  );
6848
6903
  return;
6849
6904
  }
6850
- if (sessionEnd) return;
6905
+ if (sessionEnd) {
6906
+ let closedId = body.sessionId;
6907
+ try {
6908
+ const data = await res.json();
6909
+ closedId = data?.sessionId ?? closedId;
6910
+ } catch {
6911
+ }
6912
+ try {
6913
+ if (closedId) options.onSessionClosed?.(closedId);
6914
+ } catch {
6915
+ }
6916
+ return;
6917
+ }
6851
6918
  try {
6852
6919
  const data = await res.json();
6920
+ if (postGeneration !== generation) {
6921
+ options.log?.("[DeepdotsAnalytics] sessionId of a closed session ignored:", data?.sessionId);
6922
+ return;
6923
+ }
6853
6924
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
6854
6925
  feedbackSessionId = data.sessionId;
6855
6926
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
@@ -6862,6 +6933,7 @@ function createFeedbackSink(options) {
6862
6933
  if (meta?.resetSession) {
6863
6934
  feedbackSessionId = void 0;
6864
6935
  firstPostInFlight = null;
6936
+ generation++;
6865
6937
  options.onSessionReset?.();
6866
6938
  return;
6867
6939
  }
@@ -6876,6 +6948,7 @@ function createFeedbackSink(options) {
6876
6948
  if (sessionEnd) {
6877
6949
  feedbackSessionId = void 0;
6878
6950
  firstPostInFlight = null;
6951
+ generation++;
6879
6952
  options.onSessionReset?.();
6880
6953
  } else if (!feedbackSessionId) {
6881
6954
  firstPostInFlight = sent.then(
@@ -7526,6 +7599,8 @@ var DeepdotsPopups = class {
7526
7599
  this.analytics = null;
7527
7600
  /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
7528
7601
  this.analyticsFeedbackSessionId = void 0;
7602
+ /** `init({ onFeedbackSession })`: avisa al host al abrir y cerrar cada registro. */
7603
+ this.onFeedbackSession = void 0;
7529
7604
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
7530
7605
  this.analyticsFlushTimer = void 0;
7531
7606
  /** Crash & error reporting (#14–17). Null hasta init(). */
@@ -7583,6 +7658,7 @@ var DeepdotsPopups = class {
7583
7658
  this.renderChrome = config.renderChrome ?? true;
7584
7659
  this.showProgressBar = config.showProgressBar;
7585
7660
  this.surveyCss = config.surveyCss;
7661
+ this.onFeedbackSession = config.onFeedbackSession;
7586
7662
  this.config = {
7587
7663
  apiKey: config.apiKey || void 0,
7588
7664
  debug: config.debug || false,
@@ -7616,7 +7692,9 @@ var DeepdotsPopups = class {
7616
7692
  log: (...a) => this.log(...a),
7617
7693
  onSessionId: (id) => {
7618
7694
  this.analyticsFeedbackSessionId = id;
7695
+ this.notifyFeedbackSession(id, "open");
7619
7696
  },
7697
+ onSessionClosed: (id) => this.notifyFeedbackSession(id, "closed"),
7620
7698
  onSessionReset: () => {
7621
7699
  this.analyticsFeedbackSessionId = void 0;
7622
7700
  }
@@ -7691,6 +7769,23 @@ var DeepdotsPopups = class {
7691
7769
  getSessionId() {
7692
7770
  return this.tracking?.getSessionId() ?? null;
7693
7771
  }
7772
+ /**
7773
+ * Id del registro de analytics abierto: el `sessionId` que devolvió `POST /sdk/feedback`,
7774
+ * que la API guarda como `sdkSessionId` en el Feedback en que se convierte la sesión. No es
7775
+ * `getSessionId()`. Null hasta que el backend acepta el primer lote y de nuevo al cerrar la
7776
+ * sesión: para saber también qué registro se cerró, usa `init({ onFeedbackSession })`.
7777
+ */
7778
+ getFeedbackSessionId() {
7779
+ return this.analyticsFeedbackSessionId ?? null;
7780
+ }
7781
+ /** Avisa al host del registro abierto o cerrado; un fallo del host no corta el envío. */
7782
+ notifyFeedbackSession(sessionId, status) {
7783
+ try {
7784
+ this.onFeedbackSession?.({ sessionId, status });
7785
+ } catch (error) {
7786
+ this.log("analytics \xB7 onFeedbackSession threw:", error);
7787
+ }
7788
+ }
7694
7789
  /** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
7695
7790
  setTrackingEnabled(enabled) {
7696
7791
  if (!enabled) this.closeSession("tracking_disabled");