@feedvalue/core 0.1.11 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,9 +1,63 @@
1
+ /**
2
+ * @feedvalue/core - Context Capture
3
+ *
4
+ * Captures DOM context from a trigger element for enhanced feedback metadata.
5
+ * This allows understanding WHERE on a page the user reacted, not just which page.
6
+ */
7
+ /**
8
+ * Context capture configuration
9
+ */
10
+ interface ContextCaptureConfig {
11
+ /** Enable automatic context capture (default: true) */
12
+ enabled: boolean;
13
+ /** Maximum parent traversal depth (default: 5) */
14
+ maxDepth: number;
15
+ /** Maximum heading text length (default: 100) */
16
+ maxHeadingLength: number;
17
+ /** Data attribute whitelist (default: ['data-section', 'data-feature', 'data-component']) */
18
+ dataAttributeWhitelist: string[];
19
+ }
20
+ declare const DEFAULT_CONTEXT_CAPTURE_CONFIG: ContextCaptureConfig;
21
+ /**
22
+ * Captured context data from DOM traversal
23
+ */
24
+ interface CapturedContext {
25
+ /** ID of nearest parent element with an id attribute */
26
+ sectionId?: string;
27
+ /** Tag name of the section element (e.g., 'section', 'article', 'div') */
28
+ sectionTag?: string;
29
+ /** Text content of nearest heading (h1-h6) */
30
+ nearestHeading?: string;
31
+ /** Level of the nearest heading (1-6) */
32
+ headingLevel?: number;
33
+ /** Captured data-* attributes from element and parents */
34
+ dataAttributes?: Record<string, string>;
35
+ /** CSS selector path to the trigger element (for debugging) */
36
+ cssSelector?: string;
37
+ }
38
+ /**
39
+ * Capture DOM context from a trigger element
40
+ *
41
+ * @param triggerElement - The element that triggered the reaction (e.g., the button)
42
+ * @param config - Configuration for context capture
43
+ * @returns Captured context object or null if disabled/unavailable
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Capture context from a button click
48
+ * const context = captureContext(event.currentTarget, config);
49
+ * // Returns: { sectionId: 'installation', nearestHeading: 'Installation', ... }
50
+ * ```
51
+ */
52
+ declare function captureContext(triggerElement: Element | null, config?: ContextCaptureConfig): CapturedContext | null;
53
+
1
54
  /**
2
55
  * @feedvalue/core - Type Definitions
3
56
  *
4
57
  * Core types for the FeedValue SDK. These types are shared across
5
58
  * all framework packages (React, Vue) and the vanilla API.
6
59
  */
60
+
7
61
  /**
8
62
  * Widget position on screen
9
63
  */
@@ -43,6 +97,8 @@ interface WidgetStyling {
43
97
  backgroundColor: string;
44
98
  textColor: string;
45
99
  buttonTextColor: string;
100
+ borderColor: string;
101
+ borderWidth: string;
46
102
  borderRadius: string;
47
103
  customCSS?: string | undefined;
48
104
  }
@@ -91,6 +147,14 @@ interface FeedValueOptions {
91
147
  * @default false
92
148
  */
93
149
  headless?: boolean | undefined;
150
+ /**
151
+ * Context capture configuration for reaction widgets.
152
+ * When enabled, captures DOM context (section ID, nearest heading, data attributes)
153
+ * from the trigger element when react() is called.
154
+ *
155
+ * @default { enabled: true, maxDepth: 5, maxHeadingLength: 100, dataAttributeWhitelist: ['data-section', 'data-feature', 'data-component', 'data-fv-section', 'data-fv-feature'] }
156
+ */
157
+ contextCapture?: Partial<ContextCaptureConfig> | undefined;
94
158
  }
95
159
  /**
96
160
  * Runtime configuration (can be changed after init)
@@ -425,10 +489,11 @@ interface FeedValueInstance {
425
489
  /**
426
490
  * Submit a reaction.
427
491
  * @param value - Selected reaction option value
428
- * @param options - Optional follow-up text
492
+ * @param options - Optional follow-up text and trigger element for context capture
429
493
  */
430
494
  react(value: string, options?: {
431
495
  followUp?: string;
496
+ triggerElement?: Element | null;
432
497
  }): Promise<void>;
433
498
  /**
434
499
  * Check if widget is a reaction type
@@ -450,6 +515,8 @@ interface FeedValueInstance {
450
515
  setConfig(config: Partial<FeedValueConfig>): void;
451
516
  /** Get current configuration */
452
517
  getConfig(): FeedValueConfig;
518
+ /** Get widget configuration (from API) */
519
+ getWidgetConfig(): WidgetConfig | null;
453
520
  /** Subscribe to state changes */
454
521
  subscribe(callback: () => void): () => void;
455
522
  /** Get current state snapshot */
@@ -493,6 +560,7 @@ declare class FeedValue implements FeedValueInstance {
493
560
  private readonly apiClient;
494
561
  private readonly emitter;
495
562
  private readonly headless;
563
+ private readonly contextCaptureConfig;
496
564
  private config;
497
565
  private widgetConfig;
498
566
  private state;
@@ -505,6 +573,7 @@ declare class FeedValue implements FeedValueInstance {
505
573
  private modal;
506
574
  private overlay;
507
575
  private stylesInjected;
576
+ private viewportResizeCleanup;
508
577
  private autoCloseTimeout;
509
578
  private isDestroyed;
510
579
  /**
@@ -567,10 +636,11 @@ declare class FeedValue implements FeedValueInstance {
567
636
  /**
568
637
  * Submit a reaction.
569
638
  * @param value - Selected reaction option value
570
- * @param options - Optional follow-up text
639
+ * @param options - Optional follow-up text and trigger element for context capture
571
640
  */
572
641
  react(value: string, options?: {
573
642
  followUp?: string;
643
+ triggerElement?: Element | null;
574
644
  }): Promise<void>;
575
645
  /**
576
646
  * Check if widget is a reaction type
@@ -645,8 +715,15 @@ declare class FeedValue implements FeedValueInstance {
645
715
  */
646
716
  private getModalPositionStyles;
647
717
  /**
648
- * SVG icons for trigger button (matching widget-bundle exactly)
718
+ * Smooth viewport resize compensation for mobile browser toolbar show/hide.
719
+ *
720
+ * When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
721
+ * resizes and position:fixed elements jump. This detects toolbar-sized height
722
+ * changes and applies an inverse CSS translate to cancel the jump, then animates
723
+ * back to the natural position. Uses the `translate` CSS property (separate from
724
+ * `transform`) to avoid conflicting with the hover transform on the trigger.
649
725
  */
726
+ private setupViewportCompensation;
650
727
  /**
651
728
  * SVG icons for trigger button - must match Lucide icons used in frontend-web
652
729
  * chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
@@ -848,4 +925,4 @@ declare function generateFingerprint(): string;
848
925
  */
849
926
  declare function clearFingerprint(): void;
850
927
 
851
- export { ApiClient, type ButtonSize, type ConfigResponse, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, clearFingerprint, generateFingerprint };
928
+ export { ApiClient, type ButtonSize, type CapturedContext, type ConfigResponse, type ContextCaptureConfig, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, captureContext, clearFingerprint, generateFingerprint };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,63 @@
1
+ /**
2
+ * @feedvalue/core - Context Capture
3
+ *
4
+ * Captures DOM context from a trigger element for enhanced feedback metadata.
5
+ * This allows understanding WHERE on a page the user reacted, not just which page.
6
+ */
7
+ /**
8
+ * Context capture configuration
9
+ */
10
+ interface ContextCaptureConfig {
11
+ /** Enable automatic context capture (default: true) */
12
+ enabled: boolean;
13
+ /** Maximum parent traversal depth (default: 5) */
14
+ maxDepth: number;
15
+ /** Maximum heading text length (default: 100) */
16
+ maxHeadingLength: number;
17
+ /** Data attribute whitelist (default: ['data-section', 'data-feature', 'data-component']) */
18
+ dataAttributeWhitelist: string[];
19
+ }
20
+ declare const DEFAULT_CONTEXT_CAPTURE_CONFIG: ContextCaptureConfig;
21
+ /**
22
+ * Captured context data from DOM traversal
23
+ */
24
+ interface CapturedContext {
25
+ /** ID of nearest parent element with an id attribute */
26
+ sectionId?: string;
27
+ /** Tag name of the section element (e.g., 'section', 'article', 'div') */
28
+ sectionTag?: string;
29
+ /** Text content of nearest heading (h1-h6) */
30
+ nearestHeading?: string;
31
+ /** Level of the nearest heading (1-6) */
32
+ headingLevel?: number;
33
+ /** Captured data-* attributes from element and parents */
34
+ dataAttributes?: Record<string, string>;
35
+ /** CSS selector path to the trigger element (for debugging) */
36
+ cssSelector?: string;
37
+ }
38
+ /**
39
+ * Capture DOM context from a trigger element
40
+ *
41
+ * @param triggerElement - The element that triggered the reaction (e.g., the button)
42
+ * @param config - Configuration for context capture
43
+ * @returns Captured context object or null if disabled/unavailable
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Capture context from a button click
48
+ * const context = captureContext(event.currentTarget, config);
49
+ * // Returns: { sectionId: 'installation', nearestHeading: 'Installation', ... }
50
+ * ```
51
+ */
52
+ declare function captureContext(triggerElement: Element | null, config?: ContextCaptureConfig): CapturedContext | null;
53
+
1
54
  /**
2
55
  * @feedvalue/core - Type Definitions
3
56
  *
4
57
  * Core types for the FeedValue SDK. These types are shared across
5
58
  * all framework packages (React, Vue) and the vanilla API.
6
59
  */
60
+
7
61
  /**
8
62
  * Widget position on screen
9
63
  */
@@ -43,6 +97,8 @@ interface WidgetStyling {
43
97
  backgroundColor: string;
44
98
  textColor: string;
45
99
  buttonTextColor: string;
100
+ borderColor: string;
101
+ borderWidth: string;
46
102
  borderRadius: string;
47
103
  customCSS?: string | undefined;
48
104
  }
@@ -91,6 +147,14 @@ interface FeedValueOptions {
91
147
  * @default false
92
148
  */
93
149
  headless?: boolean | undefined;
150
+ /**
151
+ * Context capture configuration for reaction widgets.
152
+ * When enabled, captures DOM context (section ID, nearest heading, data attributes)
153
+ * from the trigger element when react() is called.
154
+ *
155
+ * @default { enabled: true, maxDepth: 5, maxHeadingLength: 100, dataAttributeWhitelist: ['data-section', 'data-feature', 'data-component', 'data-fv-section', 'data-fv-feature'] }
156
+ */
157
+ contextCapture?: Partial<ContextCaptureConfig> | undefined;
94
158
  }
95
159
  /**
96
160
  * Runtime configuration (can be changed after init)
@@ -425,10 +489,11 @@ interface FeedValueInstance {
425
489
  /**
426
490
  * Submit a reaction.
427
491
  * @param value - Selected reaction option value
428
- * @param options - Optional follow-up text
492
+ * @param options - Optional follow-up text and trigger element for context capture
429
493
  */
430
494
  react(value: string, options?: {
431
495
  followUp?: string;
496
+ triggerElement?: Element | null;
432
497
  }): Promise<void>;
433
498
  /**
434
499
  * Check if widget is a reaction type
@@ -450,6 +515,8 @@ interface FeedValueInstance {
450
515
  setConfig(config: Partial<FeedValueConfig>): void;
451
516
  /** Get current configuration */
452
517
  getConfig(): FeedValueConfig;
518
+ /** Get widget configuration (from API) */
519
+ getWidgetConfig(): WidgetConfig | null;
453
520
  /** Subscribe to state changes */
454
521
  subscribe(callback: () => void): () => void;
455
522
  /** Get current state snapshot */
@@ -493,6 +560,7 @@ declare class FeedValue implements FeedValueInstance {
493
560
  private readonly apiClient;
494
561
  private readonly emitter;
495
562
  private readonly headless;
563
+ private readonly contextCaptureConfig;
496
564
  private config;
497
565
  private widgetConfig;
498
566
  private state;
@@ -505,6 +573,7 @@ declare class FeedValue implements FeedValueInstance {
505
573
  private modal;
506
574
  private overlay;
507
575
  private stylesInjected;
576
+ private viewportResizeCleanup;
508
577
  private autoCloseTimeout;
509
578
  private isDestroyed;
510
579
  /**
@@ -567,10 +636,11 @@ declare class FeedValue implements FeedValueInstance {
567
636
  /**
568
637
  * Submit a reaction.
569
638
  * @param value - Selected reaction option value
570
- * @param options - Optional follow-up text
639
+ * @param options - Optional follow-up text and trigger element for context capture
571
640
  */
572
641
  react(value: string, options?: {
573
642
  followUp?: string;
643
+ triggerElement?: Element | null;
574
644
  }): Promise<void>;
575
645
  /**
576
646
  * Check if widget is a reaction type
@@ -645,8 +715,15 @@ declare class FeedValue implements FeedValueInstance {
645
715
  */
646
716
  private getModalPositionStyles;
647
717
  /**
648
- * SVG icons for trigger button (matching widget-bundle exactly)
718
+ * Smooth viewport resize compensation for mobile browser toolbar show/hide.
719
+ *
720
+ * When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
721
+ * resizes and position:fixed elements jump. This detects toolbar-sized height
722
+ * changes and applies an inverse CSS translate to cancel the jump, then animates
723
+ * back to the natural position. Uses the `translate` CSS property (separate from
724
+ * `transform`) to avoid conflicting with the hover transform on the trigger.
649
725
  */
726
+ private setupViewportCompensation;
650
727
  /**
651
728
  * SVG icons for trigger button - must match Lucide icons used in frontend-web
652
729
  * chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
@@ -848,4 +925,4 @@ declare function generateFingerprint(): string;
848
925
  */
849
926
  declare function clearFingerprint(): void;
850
927
 
851
- export { ApiClient, type ButtonSize, type ConfigResponse, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, clearFingerprint, generateFingerprint };
928
+ export { ApiClient, type ButtonSize, type CapturedContext, type ConfigResponse, type ContextCaptureConfig, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, captureContext, clearFingerprint, generateFingerprint };
package/dist/index.js CHANGED
@@ -453,6 +453,123 @@ function clearFingerprint() {
453
453
  }
454
454
  }
455
455
 
456
+ // src/context-capture.ts
457
+ var DEFAULT_CONTEXT_CAPTURE_CONFIG = {
458
+ enabled: true,
459
+ maxDepth: 5,
460
+ maxHeadingLength: 100,
461
+ dataAttributeWhitelist: [
462
+ "data-section",
463
+ "data-feature",
464
+ "data-component",
465
+ "data-fv-section",
466
+ "data-fv-feature"
467
+ ]
468
+ };
469
+ function findNearestWithId(element, maxDepth) {
470
+ let current = element;
471
+ let depth = 0;
472
+ while (current && depth < maxDepth) {
473
+ if (current.id) {
474
+ return current;
475
+ }
476
+ current = current.parentElement;
477
+ depth++;
478
+ }
479
+ return null;
480
+ }
481
+ function findNearestHeading(element, section, maxDepth) {
482
+ if (section) {
483
+ const heading = section.querySelector("h1, h2, h3, h4, h5, h6");
484
+ if (heading) return heading;
485
+ }
486
+ let current = element;
487
+ let depth = 0;
488
+ while (current && depth < maxDepth) {
489
+ let sibling = current.previousElementSibling;
490
+ while (sibling) {
491
+ if (/^H[1-6]$/.test(sibling.tagName)) {
492
+ return sibling;
493
+ }
494
+ sibling = sibling.previousElementSibling;
495
+ }
496
+ current = current.parentElement;
497
+ depth++;
498
+ }
499
+ return null;
500
+ }
501
+ function captureDataAttributes(element, whitelist, maxDepth) {
502
+ const result = {};
503
+ let current = element;
504
+ let depth = 0;
505
+ while (current && depth < maxDepth) {
506
+ for (const attr of Array.from(current.attributes)) {
507
+ if (whitelist.includes(attr.name) && !result[attr.name]) {
508
+ result[attr.name] = attr.value;
509
+ }
510
+ }
511
+ current = current.parentElement;
512
+ depth++;
513
+ }
514
+ return result;
515
+ }
516
+ function generateSelector(element, maxDepth) {
517
+ const parts = [];
518
+ let current = element;
519
+ let depth = 0;
520
+ while (current && depth < maxDepth && current !== document.body) {
521
+ let selector = current.tagName.toLowerCase();
522
+ if (current.id) {
523
+ selector = `#${current.id}`;
524
+ parts.unshift(selector);
525
+ break;
526
+ }
527
+ if (current.className && typeof current.className === "string") {
528
+ const classes = current.className.split(" ").filter((c) => c.trim()).slice(0, 2);
529
+ if (classes.length) {
530
+ selector += "." + classes.join(".");
531
+ }
532
+ }
533
+ parts.unshift(selector);
534
+ current = current.parentElement;
535
+ depth++;
536
+ }
537
+ return parts.join(" > ");
538
+ }
539
+ function truncate(str, maxLength) {
540
+ if (str.length <= maxLength) return str;
541
+ return str.substring(0, maxLength - 3) + "...";
542
+ }
543
+ function captureContext(triggerElement, config = DEFAULT_CONTEXT_CAPTURE_CONFIG) {
544
+ if (!config.enabled || !triggerElement || typeof document === "undefined") {
545
+ return null;
546
+ }
547
+ const context = {};
548
+ const sectionWithId = findNearestWithId(triggerElement, config.maxDepth);
549
+ if (sectionWithId) {
550
+ context.sectionId = sectionWithId.id;
551
+ context.sectionTag = sectionWithId.tagName.toLowerCase();
552
+ }
553
+ const heading = findNearestHeading(triggerElement, sectionWithId, config.maxDepth);
554
+ if (heading) {
555
+ context.nearestHeading = truncate(heading.textContent?.trim() || "", config.maxHeadingLength);
556
+ const level = heading.tagName[1];
557
+ if (level) {
558
+ context.headingLevel = parseInt(level, 10);
559
+ }
560
+ }
561
+ const dataAttrs = captureDataAttributes(
562
+ triggerElement,
563
+ config.dataAttributeWhitelist,
564
+ config.maxDepth
565
+ );
566
+ if (Object.keys(dataAttrs).length > 0) {
567
+ context.dataAttributes = dataAttrs;
568
+ }
569
+ context.cssSelector = generateSelector(triggerElement, 3);
570
+ return Object.keys(context).length > 0 ? context : null;
571
+ }
572
+
456
573
  // src/feedvalue.ts
457
574
  var SUCCESS_AUTO_CLOSE_DELAY_MS = 3e3;
458
575
  var VALID_SENTIMENTS = ["angry", "disappointed", "satisfied", "excited"];
@@ -475,6 +592,7 @@ var _FeedValue = class _FeedValue {
475
592
  __publicField(this, "apiClient");
476
593
  __publicField(this, "emitter");
477
594
  __publicField(this, "headless");
595
+ __publicField(this, "contextCaptureConfig");
478
596
  __publicField(this, "config");
479
597
  __publicField(this, "widgetConfig", null);
480
598
  // State
@@ -497,6 +615,8 @@ var _FeedValue = class _FeedValue {
497
615
  __publicField(this, "modal", null);
498
616
  __publicField(this, "overlay", null);
499
617
  __publicField(this, "stylesInjected", false);
618
+ // Viewport resize compensation cleanup
619
+ __publicField(this, "viewportResizeCleanup", null);
500
620
  // Auto-close timeout reference (for cleanup on destroy)
501
621
  __publicField(this, "autoCloseTimeout", null);
502
622
  // Destroyed flag - guards async continuations (fixes React StrictMode race condition)
@@ -504,6 +624,10 @@ var _FeedValue = class _FeedValue {
504
624
  this.widgetId = options.widgetId;
505
625
  this.headless = options.headless ?? false;
506
626
  this.config = { ...DEFAULT_CONFIG, ...options.config };
627
+ this.contextCaptureConfig = {
628
+ ...DEFAULT_CONTEXT_CAPTURE_CONFIG,
629
+ ...options.contextCapture
630
+ };
507
631
  this.apiClient = new ApiClient(
508
632
  options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
509
633
  this.config.debug
@@ -567,12 +691,28 @@ var _FeedValue = class _FeedValue {
567
691
  thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
568
692
  showBranding: configResponse.config.showBranding ?? true,
569
693
  customFields: configResponse.config.customFields,
570
- // Reaction config (for reaction widgets) - only include if defined
694
+ // Reaction config (for reaction widgets) - pass through all fields
571
695
  ...configResponse.config.template && { template: configResponse.config.template },
572
696
  ...configResponse.config.options && { options: configResponse.config.options },
573
697
  followUpLabel: configResponse.config.followUpLabel ?? "Tell us more (optional)",
574
- submitText: configResponse.config.submitText ?? "Send"
698
+ submitText: configResponse.config.submitText ?? "Send",
699
+ // Reaction widget display options (support both camelCase and snake_case from API)
700
+ ...(configResponse.config.showLabels !== void 0 || configResponse.config.show_labels !== void 0) && {
701
+ showLabels: configResponse.config.showLabels ?? configResponse.config.show_labels
702
+ },
703
+ ...(configResponse.config.buttonSize || configResponse.config.button_size) && {
704
+ buttonSize: configResponse.config.buttonSize ?? configResponse.config.button_size
705
+ },
706
+ ...(configResponse.config.followUpTrigger || configResponse.config.follow_up_trigger) && {
707
+ followUpTrigger: configResponse.config.followUpTrigger ?? configResponse.config.follow_up_trigger
708
+ }
575
709
  };
710
+ this.log("Built baseConfig:", {
711
+ buttonSize: baseConfig.buttonSize,
712
+ showLabels: baseConfig.showLabels,
713
+ followUpTrigger: baseConfig.followUpTrigger,
714
+ template: baseConfig.template
715
+ });
576
716
  this.widgetConfig = {
577
717
  widgetId: configResponse.widget_id,
578
718
  widgetKey: configResponse.widget_key,
@@ -580,12 +720,16 @@ var _FeedValue = class _FeedValue {
580
720
  type: configResponse.type ?? "feedback",
581
721
  config: baseConfig,
582
722
  styling: {
583
- primaryColor: configResponse.styling.primaryColor ?? "#3b82f6",
584
- backgroundColor: configResponse.styling.backgroundColor ?? "#ffffff",
585
- textColor: configResponse.styling.textColor ?? "#1f2937",
586
- buttonTextColor: configResponse.styling.buttonTextColor ?? "#ffffff",
587
- borderRadius: configResponse.styling.borderRadius ?? "8px",
588
- customCSS: configResponse.styling.customCSS
723
+ // Pass through all styling properties from API
724
+ ...configResponse.styling,
725
+ // Apply defaults for required fields (support both camelCase and snake_case from API)
726
+ primaryColor: configResponse.styling.primaryColor ?? configResponse.styling.primary_color ?? "#3b82f6",
727
+ backgroundColor: configResponse.styling.backgroundColor ?? configResponse.styling.background_color ?? "#ffffff",
728
+ textColor: configResponse.styling.textColor ?? configResponse.styling.text_color ?? "#1f2937",
729
+ buttonTextColor: configResponse.styling.buttonTextColor ?? configResponse.styling.button_text_color ?? "#ffffff",
730
+ borderColor: configResponse.styling.borderColor ?? configResponse.styling.border_color ?? "#e5e7eb",
731
+ borderWidth: configResponse.styling.borderWidth ?? configResponse.styling.border_width ?? "1",
732
+ borderRadius: configResponse.styling.borderRadius ?? configResponse.styling.border_radius ?? "8px"
589
733
  }
590
734
  };
591
735
  if (!this.headless && typeof window !== "undefined" && typeof document !== "undefined") {
@@ -611,6 +755,8 @@ var _FeedValue = class _FeedValue {
611
755
  clearTimeout(this.autoCloseTimeout);
612
756
  this.autoCloseTimeout = null;
613
757
  }
758
+ this.viewportResizeCleanup?.();
759
+ this.viewportResizeCleanup = null;
614
760
  this.triggerButton?.remove();
615
761
  this.modal?.remove();
616
762
  this.overlay?.remove();
@@ -819,12 +965,12 @@ var _FeedValue = class _FeedValue {
819
965
  getTemplateOptions(template) {
820
966
  const templates = {
821
967
  thumbs: [
822
- { label: "Helpful", value: "helpful", icon: "thumbs-up", showFollowUp: false },
823
- { label: "Not Helpful", value: "not_helpful", icon: "thumbs-down", showFollowUp: true }
968
+ { label: "Helpful", value: "helpful", icon: "\u{1F44D}", showFollowUp: false },
969
+ { label: "Not Helpful", value: "not_helpful", icon: "\u{1F44E}", showFollowUp: true }
824
970
  ],
825
971
  helpful: [
826
- { label: "Yes", value: "yes", icon: "check", showFollowUp: false },
827
- { label: "No", value: "no", icon: "x", showFollowUp: true }
972
+ { label: "Yes", value: "yes", icon: "\u2713", showFollowUp: false },
973
+ { label: "No", value: "no", icon: "\u2717", showFollowUp: true }
828
974
  ],
829
975
  emoji: [
830
976
  { label: "Angry", value: "angry", icon: "\u{1F620}", showFollowUp: true },
@@ -846,7 +992,7 @@ var _FeedValue = class _FeedValue {
846
992
  /**
847
993
  * Submit a reaction.
848
994
  * @param value - Selected reaction option value
849
- * @param options - Optional follow-up text
995
+ * @param options - Optional follow-up text and trigger element for context capture
850
996
  */
851
997
  async react(value, options) {
852
998
  if (!this.state.isReady) {
@@ -864,13 +1010,25 @@ var _FeedValue = class _FeedValue {
864
1010
  const validValues = reactionOptions.map((opt) => opt.value).join(", ");
865
1011
  throw new Error(`Invalid reaction value. Must be one of: ${validValues}`);
866
1012
  }
1013
+ let capturedContext = null;
1014
+ if (options?.triggerElement) {
1015
+ capturedContext = captureContext(options.triggerElement, this.contextCaptureConfig);
1016
+ this.log("Captured context", capturedContext);
1017
+ }
867
1018
  this.emitter.emit("react", { value, hasFollowUp: selectedOption.showFollowUp });
868
1019
  this.updateState({ isSubmitting: true });
869
1020
  try {
870
1021
  const reactionData = {
871
1022
  value,
872
1023
  metadata: {
873
- page_url: typeof window !== "undefined" ? window.location.href : ""
1024
+ page_url: typeof window !== "undefined" ? window.location.href : "",
1025
+ // Spread captured context into metadata
1026
+ ...capturedContext?.sectionId && { section_id: capturedContext.sectionId },
1027
+ ...capturedContext?.sectionTag && { section_tag: capturedContext.sectionTag },
1028
+ ...capturedContext?.nearestHeading && { nearest_heading: capturedContext.nearestHeading },
1029
+ ...capturedContext?.headingLevel && { heading_level: capturedContext.headingLevel },
1030
+ ...capturedContext?.dataAttributes && { data_attributes: capturedContext.dataAttributes },
1031
+ ...capturedContext?.cssSelector && { css_selector: capturedContext.cssSelector }
874
1032
  },
875
1033
  ...options?.followUp && { followUp: options.followUp }
876
1034
  };
@@ -1022,6 +1180,7 @@ var _FeedValue = class _FeedValue {
1022
1180
  }
1023
1181
  this.renderTrigger();
1024
1182
  this.renderModal();
1183
+ this.setupViewportCompensation();
1025
1184
  }
1026
1185
  /**
1027
1186
  * Sanitize CSS to block potentially dangerous patterns
@@ -1234,7 +1393,7 @@ var _FeedValue = class _FeedValue {
1234
1393
  getPositionStyles(position) {
1235
1394
  switch (position) {
1236
1395
  case "bottom-left":
1237
- return "bottom: 20px; left: 20px;";
1396
+ return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
1238
1397
  case "top-right":
1239
1398
  return "top: 20px; right: 20px;";
1240
1399
  case "top-left":
@@ -1243,7 +1402,7 @@ var _FeedValue = class _FeedValue {
1243
1402
  return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
1244
1403
  case "bottom-right":
1245
1404
  default:
1246
- return "bottom: 20px; right: 20px;";
1405
+ return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
1247
1406
  }
1248
1407
  }
1249
1408
  /**
@@ -1252,9 +1411,9 @@ var _FeedValue = class _FeedValue {
1252
1411
  getModalPositionStyles(position) {
1253
1412
  switch (position) {
1254
1413
  case "bottom-left":
1255
- return "bottom: 20px; left: 20px;";
1414
+ return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
1256
1415
  case "bottom-right":
1257
- return "bottom: 20px; right: 20px;";
1416
+ return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
1258
1417
  case "top-right":
1259
1418
  return "top: 20px; right: 20px;";
1260
1419
  case "top-left":
@@ -1264,6 +1423,37 @@ var _FeedValue = class _FeedValue {
1264
1423
  return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
1265
1424
  }
1266
1425
  }
1426
+ /**
1427
+ * Smooth viewport resize compensation for mobile browser toolbar show/hide.
1428
+ *
1429
+ * When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
1430
+ * resizes and position:fixed elements jump. This detects toolbar-sized height
1431
+ * changes and applies an inverse CSS translate to cancel the jump, then animates
1432
+ * back to the natural position. Uses the `translate` CSS property (separate from
1433
+ * `transform`) to avoid conflicting with the hover transform on the trigger.
1434
+ */
1435
+ setupViewportCompensation() {
1436
+ if (typeof window === "undefined") return;
1437
+ let lastHeight = window.innerHeight;
1438
+ const handleResize = () => {
1439
+ const newHeight = window.innerHeight;
1440
+ const delta = newHeight - lastHeight;
1441
+ lastHeight = newHeight;
1442
+ if (Math.abs(delta) <= 5 || Math.abs(delta) >= 120) return;
1443
+ const elements = [this.triggerButton, this.modal].filter(Boolean);
1444
+ for (const el of elements) {
1445
+ el.style.transition = "none";
1446
+ el.style.translate = `0 ${delta}px`;
1447
+ void el.offsetHeight;
1448
+ el.style.transition = "translate 0.3s ease-out";
1449
+ el.style.translate = "0 0";
1450
+ }
1451
+ };
1452
+ window.addEventListener("resize", handleResize, { passive: true });
1453
+ this.viewportResizeCleanup = () => {
1454
+ window.removeEventListener("resize", handleResize);
1455
+ };
1456
+ }
1267
1457
  /**
1268
1458
  * Parse SVG string to DOM element safely using DOMParser
1269
1459
  */
@@ -1464,9 +1654,6 @@ var _FeedValue = class _FeedValue {
1464
1654
  }
1465
1655
  }
1466
1656
  };
1467
- /**
1468
- * SVG icons for trigger button (matching widget-bundle exactly)
1469
- */
1470
1657
  /**
1471
1658
  * SVG icons for trigger button - must match Lucide icons used in frontend-web
1472
1659
  * chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
@@ -1490,6 +1677,6 @@ var NEGATIVE_OPTIONS_MAP = {
1490
1677
  rating: ["1", "2"]
1491
1678
  };
1492
1679
 
1493
- export { ApiClient, DEFAULT_API_BASE_URL, FeedValue, NEGATIVE_OPTIONS_MAP, TypedEventEmitter, clearFingerprint, generateFingerprint };
1680
+ export { ApiClient, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, FeedValue, NEGATIVE_OPTIONS_MAP, TypedEventEmitter, captureContext, clearFingerprint, generateFingerprint };
1494
1681
  //# sourceMappingURL=index.js.map
1495
1682
  //# sourceMappingURL=index.js.map