@seekora-ai/ui-sdk-react 0.2.0 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/FederatedDropdown.d.ts +6 -0
  2. package/dist/components/FederatedDropdown.d.ts.map +1 -1
  3. package/dist/components/FederatedDropdown.js +4 -2
  4. package/dist/components/SearchBarWithSuggestions.d.ts +4 -0
  5. package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -1
  6. package/dist/components/SearchBarWithSuggestions.js +2 -2
  7. package/dist/components/SearchResults.d.ts.map +1 -1
  8. package/dist/components/SearchResults.js +24 -58
  9. package/dist/docsearch/components/DocSearch.d.ts +4 -0
  10. package/dist/docsearch/components/DocSearch.d.ts.map +1 -0
  11. package/dist/docsearch/components/DocSearch.js +91 -0
  12. package/dist/docsearch/components/DocSearchButton.d.ts +4 -0
  13. package/dist/docsearch/components/DocSearchButton.d.ts.map +1 -0
  14. package/dist/docsearch/components/DocSearchButton.js +12 -0
  15. package/dist/docsearch/components/Footer.d.ts +8 -0
  16. package/dist/docsearch/components/Footer.d.ts.map +1 -0
  17. package/dist/docsearch/components/Footer.js +23 -0
  18. package/dist/docsearch/components/Highlight.d.ts +9 -0
  19. package/dist/docsearch/components/Highlight.d.ts.map +1 -0
  20. package/dist/docsearch/components/Highlight.js +48 -0
  21. package/dist/docsearch/components/Hit.d.ts +15 -0
  22. package/dist/docsearch/components/Hit.d.ts.map +1 -0
  23. package/dist/docsearch/components/Hit.js +96 -0
  24. package/dist/docsearch/components/Modal.d.ts +9 -0
  25. package/dist/docsearch/components/Modal.d.ts.map +1 -0
  26. package/dist/docsearch/components/Modal.js +54 -0
  27. package/dist/docsearch/components/Results.d.ts +23 -0
  28. package/dist/docsearch/components/Results.d.ts.map +1 -0
  29. package/dist/docsearch/components/Results.js +145 -0
  30. package/dist/docsearch/components/SearchBox.d.ts +12 -0
  31. package/dist/docsearch/components/SearchBox.d.ts.map +1 -0
  32. package/dist/docsearch/components/SearchBox.js +18 -0
  33. package/dist/docsearch/hooks/useDocSearch.d.ts +33 -0
  34. package/dist/docsearch/hooks/useDocSearch.d.ts.map +1 -0
  35. package/dist/docsearch/hooks/useDocSearch.js +211 -0
  36. package/dist/docsearch/hooks/useKeyboard.d.ts +17 -0
  37. package/dist/docsearch/hooks/useKeyboard.d.ts.map +1 -0
  38. package/dist/docsearch/hooks/useKeyboard.js +71 -0
  39. package/dist/docsearch/hooks/useSeekoraSearch.d.ts +27 -0
  40. package/dist/docsearch/hooks/useSeekoraSearch.d.ts.map +1 -0
  41. package/dist/docsearch/hooks/useSeekoraSearch.js +207 -0
  42. package/dist/docsearch/index.d.ts +13 -0
  43. package/dist/docsearch/index.d.ts.map +1 -0
  44. package/dist/docsearch/index.js +11 -0
  45. package/dist/docsearch/types.d.ts +172 -0
  46. package/dist/docsearch/types.d.ts.map +1 -0
  47. package/dist/docsearch/types.js +4 -0
  48. package/dist/docsearch.css +237 -0
  49. package/dist/hooks/useAnalytics.d.ts +8 -4
  50. package/dist/hooks/useAnalytics.d.ts.map +1 -1
  51. package/dist/hooks/useAnalytics.js +14 -9
  52. package/dist/hooks/useQuerySuggestionsEnhanced.js +1 -1
  53. package/dist/hooks/useSuggestionsAnalytics.d.ts +3 -1
  54. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  55. package/dist/hooks/useSuggestionsAnalytics.js +11 -9
  56. package/dist/index.d.ts +4 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +11 -0
  59. package/dist/index.umd.js +1 -1
  60. package/dist/src/index.d.ts +271 -7
  61. package/dist/src/index.esm.js +1705 -84
  62. package/dist/src/index.esm.js.map +1 -1
  63. package/dist/src/index.js +1712 -83
  64. package/dist/src/index.js.map +1 -1
  65. package/package.json +9 -6
  66. package/src/docsearch/docsearch.css +237 -0
@@ -1,4 +1,6 @@
1
- import React, { createContext, useContext, useMemo, useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
1
+ import React, { createContext, useContext, useMemo, useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useReducer } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { SeekoraClient } from '@seekora-ai/search-sdk';
2
4
 
3
5
  /**
4
6
  * Field Mapping Utilities
@@ -565,6 +567,688 @@ class SearchStateManager {
565
567
  }
566
568
  }
567
569
 
570
+ /**
571
+ * Seekora Device Fingerprinting Module
572
+ *
573
+ * A lightweight, dependency-free device fingerprinting solution that collects
574
+ * various browser and device signals to generate a unique visitor identifier.
575
+ */
576
+ // ============================================================================
577
+ // MurmurHash3 Implementation (32-bit)
578
+ // ============================================================================
579
+ function murmurHash3(str, seed = 0) {
580
+ const remainder = str.length & 3; // str.length % 4
581
+ const bytes = str.length - remainder;
582
+ let h1 = seed;
583
+ const c1 = 0xcc9e2d51;
584
+ const c2 = 0x1b873593;
585
+ let i = 0;
586
+ let k1;
587
+ while (i < bytes) {
588
+ k1 =
589
+ (str.charCodeAt(i) & 0xff) |
590
+ ((str.charCodeAt(++i) & 0xff) << 8) |
591
+ ((str.charCodeAt(++i) & 0xff) << 16) |
592
+ ((str.charCodeAt(++i) & 0xff) << 24);
593
+ ++i;
594
+ k1 = Math.imul(k1, c1);
595
+ k1 = (k1 << 15) | (k1 >>> 17);
596
+ k1 = Math.imul(k1, c2);
597
+ h1 ^= k1;
598
+ h1 = (h1 << 13) | (h1 >>> 19);
599
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
600
+ }
601
+ k1 = 0;
602
+ switch (remainder) {
603
+ case 3:
604
+ k1 ^= (str.charCodeAt(i + 2) & 0xff) << 16;
605
+ // fallthrough
606
+ case 2:
607
+ k1 ^= (str.charCodeAt(i + 1) & 0xff) << 8;
608
+ // fallthrough
609
+ case 1:
610
+ k1 ^= str.charCodeAt(i) & 0xff;
611
+ k1 = Math.imul(k1, c1);
612
+ k1 = (k1 << 15) | (k1 >>> 17);
613
+ k1 = Math.imul(k1, c2);
614
+ h1 ^= k1;
615
+ }
616
+ h1 ^= str.length;
617
+ h1 ^= h1 >>> 16;
618
+ h1 = Math.imul(h1, 0x85ebca6b);
619
+ h1 ^= h1 >>> 13;
620
+ h1 = Math.imul(h1, 0xc2b2ae35);
621
+ h1 ^= h1 >>> 16;
622
+ return h1 >>> 0;
623
+ }
624
+ function murmurHash3Hex(str, seed = 0) {
625
+ return murmurHash3(str, seed).toString(16).padStart(8, '0');
626
+ }
627
+ // ============================================================================
628
+ // Default Configuration
629
+ // ============================================================================
630
+ const DEFAULT_CONFIG = {
631
+ enableCanvas: true,
632
+ enableWebGL: true,
633
+ enableAudio: true,
634
+ enableFonts: true,
635
+ enableHardware: true,
636
+ timeout: 5000,
637
+ };
638
+ // ============================================================================
639
+ // Font List for Detection
640
+ // ============================================================================
641
+ const FONTS_TO_CHECK = [
642
+ // Windows fonts
643
+ 'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS',
644
+ 'Consolas', 'Courier New', 'Georgia', 'Impact', 'Lucida Console',
645
+ 'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype',
646
+ 'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',
647
+ // macOS fonts
648
+ 'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',
649
+ 'Apple SD Gothic Neo', 'Arial Hebrew', 'Avenir', 'Baskerville',
650
+ 'Big Caslon', 'Brush Script MT', 'Chalkboard', 'Cochin', 'Copperplate',
651
+ 'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica', 'Helvetica Neue',
652
+ 'Herculanum', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo',
653
+ 'Monaco', 'Noteworthy', 'Optima', 'Papyrus', 'Phosphate', 'Rockwell',
654
+ 'San Francisco', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand',
655
+ 'Zapfino',
656
+ // Linux fonts
657
+ 'Cantarell', 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif',
658
+ 'Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'FreeMono', 'FreeSans',
659
+ 'FreeSerif', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
660
+ 'Noto Sans', 'Noto Serif', 'Open Sans', 'Roboto', 'Ubuntu', 'Ubuntu Mono',
661
+ // Common web fonts
662
+ 'Lato', 'Montserrat', 'Oswald', 'Raleway', 'Source Sans Pro',
663
+ 'PT Sans', 'Merriweather', 'Nunito', 'Playfair Display', 'Poppins',
664
+ ];
665
+ // ============================================================================
666
+ // Fingerprint Class
667
+ // ============================================================================
668
+ class Fingerprint {
669
+ constructor(config) {
670
+ this.cachedResult = null;
671
+ this.config = { ...DEFAULT_CONFIG, ...config };
672
+ }
673
+ /**
674
+ * Get the complete fingerprint result
675
+ */
676
+ async get() {
677
+ if (this.cachedResult) {
678
+ return this.cachedResult;
679
+ }
680
+ const components = await this.collectComponents();
681
+ const confidence = this.calculateConfidence(components);
682
+ const visitorId = this.generateVisitorId(components);
683
+ this.cachedResult = {
684
+ visitorId,
685
+ confidence,
686
+ components,
687
+ };
688
+ return this.cachedResult;
689
+ }
690
+ /**
691
+ * Clear the cached result to force re-fingerprinting
692
+ */
693
+ clearCache() {
694
+ this.cachedResult = null;
695
+ }
696
+ /**
697
+ * Collect all fingerprint components
698
+ */
699
+ async collectComponents() {
700
+ const [canvas, webgl, audio, adBlockerDetected, incognitoDetected,] = await Promise.all([
701
+ this.config.enableCanvas ? this.getCanvasFingerprint() : { hash: '' },
702
+ this.config.enableWebGL ? this.getWebGLFingerprint() : { renderer: '', vendor: '', hash: '' },
703
+ this.config.enableAudio ? this.getAudioFingerprint() : { hash: '' },
704
+ this.detectAdBlocker(),
705
+ this.detectIncognito(),
706
+ ]);
707
+ const fonts = this.config.enableFonts ? this.detectFonts() : [];
708
+ const hardware = this.config.enableHardware ? this.getHardwareInfo() : {
709
+ concurrency: 0,
710
+ deviceMemory: 0,
711
+ maxTouchPoints: 0,
712
+ platform: '',
713
+ };
714
+ const screen = this.getScreenInfo();
715
+ const browser = this.getBrowserInfo();
716
+ return {
717
+ canvas,
718
+ webgl,
719
+ audio,
720
+ fonts,
721
+ hardware,
722
+ screen,
723
+ browser,
724
+ adBlockerDetected,
725
+ incognitoDetected,
726
+ };
727
+ }
728
+ /**
729
+ * Canvas fingerprinting - draws various shapes and text, then hashes the result
730
+ */
731
+ async getCanvasFingerprint() {
732
+ try {
733
+ const canvas = document.createElement('canvas');
734
+ canvas.width = 256;
735
+ canvas.height = 128;
736
+ const ctx = canvas.getContext('2d');
737
+ if (!ctx) {
738
+ return { hash: '' };
739
+ }
740
+ // Draw background
741
+ ctx.fillStyle = '#f8f8f8';
742
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
743
+ // Draw gradient
744
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
745
+ gradient.addColorStop(0, '#ff6b6b');
746
+ gradient.addColorStop(0.5, '#4ecdc4');
747
+ gradient.addColorStop(1, '#45b7d1');
748
+ ctx.fillStyle = gradient;
749
+ ctx.fillRect(10, 10, 100, 50);
750
+ // Draw text with different fonts
751
+ ctx.font = '18px Arial';
752
+ ctx.fillStyle = '#333';
753
+ ctx.textBaseline = 'alphabetic';
754
+ ctx.fillText('Seekora Fingerprint 🔐', 20, 90);
755
+ // Draw more text with different styling
756
+ ctx.font = 'bold 14px Georgia';
757
+ ctx.fillStyle = 'rgba(102, 204, 153, 0.7)';
758
+ ctx.fillText('Test String @#$%', 130, 40);
759
+ // Draw shapes
760
+ ctx.beginPath();
761
+ ctx.arc(200, 70, 30, 0, Math.PI * 2);
762
+ ctx.fillStyle = 'rgba(255, 165, 0, 0.5)';
763
+ ctx.fill();
764
+ ctx.strokeStyle = '#333';
765
+ ctx.lineWidth = 2;
766
+ ctx.stroke();
767
+ // Draw bezier curve
768
+ ctx.beginPath();
769
+ ctx.moveTo(10, 120);
770
+ ctx.bezierCurveTo(50, 80, 100, 120, 140, 100);
771
+ ctx.strokeStyle = '#9b59b6';
772
+ ctx.lineWidth = 3;
773
+ ctx.stroke();
774
+ // Draw rotated rectangle
775
+ ctx.save();
776
+ ctx.translate(180, 100);
777
+ ctx.rotate(0.5);
778
+ ctx.fillStyle = 'rgba(52, 152, 219, 0.6)';
779
+ ctx.fillRect(-20, -10, 40, 20);
780
+ ctx.restore();
781
+ const data = canvas.toDataURL('image/png');
782
+ const hash = murmurHash3Hex(data);
783
+ return { hash, data };
784
+ }
785
+ catch {
786
+ return { hash: '' };
787
+ }
788
+ }
789
+ /**
790
+ * WebGL fingerprinting - extracts GPU info and renders test triangles
791
+ */
792
+ async getWebGLFingerprint() {
793
+ try {
794
+ const canvas = document.createElement('canvas');
795
+ canvas.width = 256;
796
+ canvas.height = 256;
797
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
798
+ if (!gl) {
799
+ return { renderer: '', vendor: '', hash: '' };
800
+ }
801
+ // Get debug info extension for renderer/vendor
802
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
803
+ const renderer = debugInfo
804
+ ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || ''
805
+ : '';
806
+ const vendor = debugInfo
807
+ ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || ''
808
+ : '';
809
+ // Collect WebGL parameters
810
+ const params = [];
811
+ // Basic parameters
812
+ const paramIds = [
813
+ gl.ALIASED_LINE_WIDTH_RANGE,
814
+ gl.ALIASED_POINT_SIZE_RANGE,
815
+ gl.ALPHA_BITS,
816
+ gl.BLUE_BITS,
817
+ gl.DEPTH_BITS,
818
+ gl.GREEN_BITS,
819
+ gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS,
820
+ gl.MAX_CUBE_MAP_TEXTURE_SIZE,
821
+ gl.MAX_FRAGMENT_UNIFORM_VECTORS,
822
+ gl.MAX_RENDERBUFFER_SIZE,
823
+ gl.MAX_TEXTURE_IMAGE_UNITS,
824
+ gl.MAX_TEXTURE_SIZE,
825
+ gl.MAX_VARYING_VECTORS,
826
+ gl.MAX_VERTEX_ATTRIBS,
827
+ gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS,
828
+ gl.MAX_VERTEX_UNIFORM_VECTORS,
829
+ gl.MAX_VIEWPORT_DIMS,
830
+ gl.RED_BITS,
831
+ gl.RENDERER,
832
+ gl.SHADING_LANGUAGE_VERSION,
833
+ gl.STENCIL_BITS,
834
+ gl.VENDOR,
835
+ gl.VERSION,
836
+ ];
837
+ for (const paramId of paramIds) {
838
+ try {
839
+ const value = gl.getParameter(paramId);
840
+ if (value !== null && value !== undefined) {
841
+ if (value instanceof Float32Array || value instanceof Int32Array) {
842
+ params.push(Array.from(value).join(','));
843
+ }
844
+ else {
845
+ params.push(String(value));
846
+ }
847
+ }
848
+ }
849
+ catch {
850
+ // Parameter not available
851
+ }
852
+ }
853
+ // Get supported extensions
854
+ const extensions = gl.getSupportedExtensions() || [];
855
+ params.push(extensions.sort().join(','));
856
+ // Render a test triangle and get pixel data
857
+ try {
858
+ const vertexShaderSource = `
859
+ attribute vec2 position;
860
+ void main() {
861
+ gl_Position = vec4(position, 0.0, 1.0);
862
+ }
863
+ `;
864
+ const fragmentShaderSource = `
865
+ precision mediump float;
866
+ void main() {
867
+ gl_FragColor = vec4(0.812, 0.373, 0.784, 1.0);
868
+ }
869
+ `;
870
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
871
+ gl.shaderSource(vertexShader, vertexShaderSource);
872
+ gl.compileShader(vertexShader);
873
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
874
+ gl.shaderSource(fragmentShader, fragmentShaderSource);
875
+ gl.compileShader(fragmentShader);
876
+ const program = gl.createProgram();
877
+ gl.attachShader(program, vertexShader);
878
+ gl.attachShader(program, fragmentShader);
879
+ gl.linkProgram(program);
880
+ gl.useProgram(program);
881
+ const vertices = new Float32Array([
882
+ 0.0, 0.5,
883
+ -0.5, -0.5,
884
+ 0.5, -0.5,
885
+ ]);
886
+ const buffer = gl.createBuffer();
887
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
888
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
889
+ const positionLocation = gl.getAttribLocation(program, 'position');
890
+ gl.enableVertexAttribArray(positionLocation);
891
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
892
+ gl.clearColor(0.1, 0.1, 0.1, 1.0);
893
+ gl.clear(gl.COLOR_BUFFER_BIT);
894
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
895
+ const pixels = new Uint8Array(256 * 256 * 4);
896
+ gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
897
+ // Sample some pixels for the hash
898
+ const sampleIndices = [0, 1000, 5000, 10000, 25000, 50000, 100000, 200000];
899
+ for (const idx of sampleIndices) {
900
+ if (idx < pixels.length) {
901
+ params.push(String(pixels[idx]));
902
+ }
903
+ }
904
+ }
905
+ catch {
906
+ // Shader compilation/rendering failed
907
+ }
908
+ const dataString = `${renderer}|${vendor}|${params.join('|')}`;
909
+ const hash = murmurHash3Hex(dataString);
910
+ return { renderer, vendor, hash };
911
+ }
912
+ catch {
913
+ return { renderer: '', vendor: '', hash: '' };
914
+ }
915
+ }
916
+ /**
917
+ * Audio fingerprinting using AudioContext oscillator
918
+ */
919
+ async getAudioFingerprint() {
920
+ try {
921
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
922
+ if (!AudioContext) {
923
+ return { hash: '' };
924
+ }
925
+ const context = new AudioContext();
926
+ // Create oscillator
927
+ const oscillator = context.createOscillator();
928
+ oscillator.type = 'triangle';
929
+ oscillator.frequency.setValueAtTime(10000, context.currentTime);
930
+ // Create compressor
931
+ const compressor = context.createDynamicsCompressor();
932
+ compressor.threshold.setValueAtTime(-50, context.currentTime);
933
+ compressor.knee.setValueAtTime(40, context.currentTime);
934
+ compressor.ratio.setValueAtTime(12, context.currentTime);
935
+ compressor.attack.setValueAtTime(0, context.currentTime);
936
+ compressor.release.setValueAtTime(0.25, context.currentTime);
937
+ // Create analyser
938
+ const analyser = context.createAnalyser();
939
+ analyser.fftSize = 2048;
940
+ // Connect nodes
941
+ oscillator.connect(compressor);
942
+ compressor.connect(analyser);
943
+ analyser.connect(context.destination);
944
+ // Start and get data
945
+ oscillator.start(0);
946
+ return new Promise((resolve) => {
947
+ const timeout = setTimeout(() => {
948
+ try {
949
+ oscillator.stop();
950
+ context.close();
951
+ }
952
+ catch {
953
+ // Ignore cleanup errors
954
+ }
955
+ resolve({ hash: '' });
956
+ }, this.config.timeout);
957
+ // Give it a moment to process
958
+ setTimeout(() => {
959
+ try {
960
+ const dataArray = new Float32Array(analyser.frequencyBinCount);
961
+ analyser.getFloatFrequencyData(dataArray);
962
+ // Calculate a fingerprint value from the audio data
963
+ let sum = 0;
964
+ let count = 0;
965
+ for (let i = 0; i < dataArray.length; i++) {
966
+ if (isFinite(dataArray[i]) && dataArray[i] !== 0) {
967
+ sum += Math.abs(dataArray[i]);
968
+ count++;
969
+ }
970
+ }
971
+ const value = count > 0 ? sum / count : 0;
972
+ const dataString = Array.from(dataArray.slice(0, 100))
973
+ .filter(v => isFinite(v))
974
+ .map(v => v.toFixed(4))
975
+ .join(',');
976
+ clearTimeout(timeout);
977
+ oscillator.stop();
978
+ context.close();
979
+ const hash = murmurHash3Hex(dataString + String(value));
980
+ resolve({ hash, value });
981
+ }
982
+ catch {
983
+ clearTimeout(timeout);
984
+ try {
985
+ oscillator.stop();
986
+ context.close();
987
+ }
988
+ catch {
989
+ // Ignore cleanup errors
990
+ }
991
+ resolve({ hash: '' });
992
+ }
993
+ }, 200);
994
+ });
995
+ }
996
+ catch {
997
+ return { hash: '' };
998
+ }
999
+ }
1000
+ /**
1001
+ * Font detection by measuring text rendering differences
1002
+ */
1003
+ detectFonts() {
1004
+ const detectedFonts = [];
1005
+ try {
1006
+ const baseFonts = ['monospace', 'sans-serif', 'serif'];
1007
+ const testString = 'mmmmmmmmmmlli';
1008
+ const testSize = '72px';
1009
+ const canvas = document.createElement('canvas');
1010
+ canvas.width = 500;
1011
+ canvas.height = 100;
1012
+ const ctx = canvas.getContext('2d');
1013
+ if (!ctx) {
1014
+ return detectedFonts;
1015
+ }
1016
+ // Get base widths
1017
+ const baseWidths = {};
1018
+ for (const baseFont of baseFonts) {
1019
+ ctx.font = `${testSize} ${baseFont}`;
1020
+ baseWidths[baseFont] = ctx.measureText(testString).width;
1021
+ }
1022
+ // Check each font
1023
+ for (const font of FONTS_TO_CHECK) {
1024
+ let detected = false;
1025
+ for (const baseFont of baseFonts) {
1026
+ ctx.font = `${testSize} "${font}", ${baseFont}`;
1027
+ const width = ctx.measureText(testString).width;
1028
+ if (width !== baseWidths[baseFont]) {
1029
+ detected = true;
1030
+ break;
1031
+ }
1032
+ }
1033
+ if (detected) {
1034
+ detectedFonts.push(font);
1035
+ }
1036
+ }
1037
+ }
1038
+ catch {
1039
+ // Font detection failed
1040
+ }
1041
+ return detectedFonts;
1042
+ }
1043
+ /**
1044
+ * Get hardware information
1045
+ */
1046
+ getHardwareInfo() {
1047
+ const nav = navigator;
1048
+ return {
1049
+ concurrency: nav.hardwareConcurrency || 0,
1050
+ deviceMemory: nav.deviceMemory || 0,
1051
+ maxTouchPoints: nav.maxTouchPoints || 0,
1052
+ platform: nav.platform || '',
1053
+ };
1054
+ }
1055
+ /**
1056
+ * Get screen information
1057
+ */
1058
+ getScreenInfo() {
1059
+ return {
1060
+ width: screen.width || 0,
1061
+ height: screen.height || 0,
1062
+ colorDepth: screen.colorDepth || 0,
1063
+ pixelRatio: window.devicePixelRatio || 1,
1064
+ };
1065
+ }
1066
+ /**
1067
+ * Get browser information
1068
+ */
1069
+ getBrowserInfo() {
1070
+ const nav = navigator;
1071
+ return {
1072
+ language: nav.language || '',
1073
+ languages: Array.from(nav.languages || []),
1074
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
1075
+ timezoneOffset: new Date().getTimezoneOffset(),
1076
+ cookieEnabled: nav.cookieEnabled ?? false,
1077
+ doNotTrack: nav.doNotTrack === '1' || nav.doNotTrack === 'yes',
1078
+ };
1079
+ }
1080
+ /**
1081
+ * Detect if an ad blocker is installed
1082
+ */
1083
+ async detectAdBlocker() {
1084
+ try {
1085
+ // Create a bait element
1086
+ const bait = document.createElement('div');
1087
+ bait.className = 'adsbox ad-banner ad_banner textAd text_ad text-ad';
1088
+ bait.style.cssText = 'position: absolute; left: -9999px; width: 1px; height: 1px;';
1089
+ bait.innerHTML = '&nbsp;';
1090
+ document.body.appendChild(bait);
1091
+ // Wait a bit for ad blockers to act
1092
+ await new Promise(resolve => setTimeout(resolve, 100));
1093
+ const isBlocked = bait.offsetParent === null ||
1094
+ bait.offsetHeight === 0 ||
1095
+ bait.offsetWidth === 0 ||
1096
+ getComputedStyle(bait).display === 'none' ||
1097
+ getComputedStyle(bait).visibility === 'hidden';
1098
+ document.body.removeChild(bait);
1099
+ // Also try to fetch a known ad-related resource
1100
+ try {
1101
+ const response = await fetch('https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', {
1102
+ method: 'HEAD',
1103
+ mode: 'no-cors',
1104
+ });
1105
+ // If we get here without error, no ad blocker (though no-cors doesn't give us much info)
1106
+ return isBlocked;
1107
+ }
1108
+ catch {
1109
+ return true; // Blocked
1110
+ }
1111
+ }
1112
+ catch {
1113
+ return false;
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Detect if browser is in incognito/private mode
1118
+ */
1119
+ async detectIncognito() {
1120
+ // Chrome/Chromium detection
1121
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
1122
+ try {
1123
+ const estimate = await navigator.storage.estimate();
1124
+ // In incognito, quota is typically limited to ~120MB
1125
+ if (estimate.quota && estimate.quota < 130 * 1024 * 1024) {
1126
+ return true;
1127
+ }
1128
+ }
1129
+ catch {
1130
+ // Storage API not available
1131
+ }
1132
+ }
1133
+ // Firefox detection using IndexedDB
1134
+ try {
1135
+ const db = indexedDB.open('test-private-mode');
1136
+ return new Promise((resolve) => {
1137
+ db.onerror = () => resolve(true);
1138
+ db.onsuccess = () => {
1139
+ db.result.close();
1140
+ indexedDB.deleteDatabase('test-private-mode');
1141
+ resolve(false);
1142
+ };
1143
+ // Timeout fallback
1144
+ setTimeout(() => resolve(false), 500);
1145
+ });
1146
+ }
1147
+ catch {
1148
+ // IndexedDB not available - might be incognito
1149
+ return true;
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Generate a hash from any data
1154
+ */
1155
+ generateHash(data) {
1156
+ return murmurHash3Hex(data);
1157
+ }
1158
+ /**
1159
+ * Generate the final visitor ID from all components
1160
+ */
1161
+ generateVisitorId(components) {
1162
+ const data = JSON.stringify({
1163
+ canvas: components.canvas.hash,
1164
+ webgl: components.webgl.hash,
1165
+ audio: components.audio.hash,
1166
+ fonts: components.fonts.sort().join(','),
1167
+ hardware: components.hardware,
1168
+ screen: components.screen,
1169
+ browser: {
1170
+ ...components.browser,
1171
+ languages: components.browser.languages.sort().join(','),
1172
+ },
1173
+ });
1174
+ // Generate a 32-character hex string (similar to UUID format)
1175
+ const hash1 = murmurHash3Hex(data, 0);
1176
+ const hash2 = murmurHash3Hex(data, 1);
1177
+ const hash3 = murmurHash3Hex(data, 2);
1178
+ const hash4 = murmurHash3Hex(data, 3);
1179
+ return `${hash1}${hash2}${hash3}${hash4}`;
1180
+ }
1181
+ /**
1182
+ * Calculate confidence score based on available components
1183
+ */
1184
+ calculateConfidence(components) {
1185
+ let score = 0;
1186
+ let maxScore = 0;
1187
+ // Canvas (high entropy)
1188
+ maxScore += 0.2;
1189
+ if (components.canvas.hash) {
1190
+ score += 0.2;
1191
+ }
1192
+ // WebGL (high entropy)
1193
+ maxScore += 0.2;
1194
+ if (components.webgl.hash && components.webgl.renderer) {
1195
+ score += 0.2;
1196
+ }
1197
+ else if (components.webgl.hash) {
1198
+ score += 0.1;
1199
+ }
1200
+ // Audio (medium entropy)
1201
+ maxScore += 0.15;
1202
+ if (components.audio.hash) {
1203
+ score += 0.15;
1204
+ }
1205
+ // Fonts (high entropy)
1206
+ maxScore += 0.15;
1207
+ if (components.fonts.length > 10) {
1208
+ score += 0.15;
1209
+ }
1210
+ else if (components.fonts.length > 5) {
1211
+ score += 0.1;
1212
+ }
1213
+ else if (components.fonts.length > 0) {
1214
+ score += 0.05;
1215
+ }
1216
+ // Hardware (medium entropy)
1217
+ maxScore += 0.1;
1218
+ if (components.hardware.concurrency > 0 && components.hardware.platform) {
1219
+ score += 0.1;
1220
+ }
1221
+ else if (components.hardware.platform) {
1222
+ score += 0.05;
1223
+ }
1224
+ // Screen (low entropy but stable)
1225
+ maxScore += 0.1;
1226
+ if (components.screen.width > 0 && components.screen.height > 0) {
1227
+ score += 0.1;
1228
+ }
1229
+ // Browser (low entropy but stable)
1230
+ maxScore += 0.1;
1231
+ if (components.browser.timezone && components.browser.language) {
1232
+ score += 0.1;
1233
+ }
1234
+ else if (components.browser.language) {
1235
+ score += 0.05;
1236
+ }
1237
+ // Normalize and return
1238
+ return Math.min(1, score / maxScore);
1239
+ }
1240
+ }
1241
+ // ============================================================================
1242
+ // Convenience Function
1243
+ // ============================================================================
1244
+ /**
1245
+ * Quick fingerprint getter - creates a Fingerprint instance and returns the result
1246
+ */
1247
+ async function getFingerprint(config) {
1248
+ const fingerprint = new Fingerprint(config);
1249
+ return fingerprint.get();
1250
+ }
1251
+
568
1252
  /**
569
1253
  * Default Theme
570
1254
  */
@@ -1133,38 +1817,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
1133
1817
  const state = { currentPage, itemsPerPage: stateItemsPerPage };
1134
1818
  // Calculate absolute position (1-based) accounting for pagination
1135
1819
  const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
1136
- // Build search context from current state and response
1137
- const searchContext = results?.context || (results ? {
1138
- query: '', // Will be filled from state if needed
1139
- page: state.currentPage,
1140
- } : undefined);
1141
- console.log('🔵 SearchResults: Tracking analytics event', {
1142
- resultId: result.id,
1143
- resultIndex: index,
1144
- absolutePosition,
1145
- currentPage: state.currentPage,
1146
- itemsPerPage: state.itemsPerPage,
1147
- hasContext: !!searchContext,
1148
- });
1149
- await client.trackEvent?.({
1150
- event_name: 'product_click',
1151
- clicked_item_id: result.id,
1152
- metadata: {
1153
- result: result,
1154
- position: absolutePosition,
1155
- },
1156
- }, searchContext);
1157
- console.log('🟢 SearchResults: Analytics event tracked successfully', {
1158
- resultId: result.id,
1159
- position: absolutePosition,
1160
- });
1820
+ const searchContext = results?.context;
1821
+ if (client.trackClick) {
1822
+ await client.trackClick(result.id, absolutePosition, searchContext);
1823
+ }
1824
+ else {
1825
+ await client.trackEvent?.({
1826
+ event_name: 'product_click',
1827
+ clicked_item_id: result.id,
1828
+ metadata: { result, position: absolutePosition },
1829
+ }, searchContext);
1830
+ }
1161
1831
  }
1162
1832
  catch (err) {
1163
1833
  const error = err instanceof Error ? err : new Error(String(err));
1164
- console.error('SearchResults: Error tracking analytics event', {
1165
- resultId: result.id,
1166
- error: error.message,
1167
- });
1834
+ log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
1168
1835
  }
1169
1836
  }
1170
1837
  // Call user-provided callback
@@ -1224,38 +1891,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
1224
1891
  const state = { currentPage, itemsPerPage: stateItemsPerPage };
1225
1892
  // Calculate absolute position (1-based) accounting for pagination
1226
1893
  const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
1227
- // Build search context from current state and response
1228
- const searchContext = results?.context || (results ? {
1229
- query: '', // Will be filled from state if needed
1230
- page: state.currentPage,
1231
- } : undefined);
1232
- console.log('🔵 SearchResults: Tracking analytics event', {
1233
- resultId: result.id,
1234
- resultIndex: index,
1235
- absolutePosition,
1236
- currentPage: state.currentPage,
1237
- itemsPerPage: state.itemsPerPage,
1238
- hasContext: !!searchContext,
1239
- });
1240
- await client.trackEvent?.({
1241
- event_name: 'product_click',
1242
- clicked_item_id: result.id,
1243
- metadata: {
1244
- result: result,
1245
- position: absolutePosition,
1246
- },
1247
- }, searchContext);
1248
- console.log('🟢 SearchResults: Analytics event tracked successfully', {
1249
- resultId: result.id,
1250
- position: absolutePosition,
1251
- });
1894
+ const searchContext = results?.context;
1895
+ if (client.trackClick) {
1896
+ await client.trackClick(result.id, absolutePosition, searchContext);
1897
+ }
1898
+ else {
1899
+ await client.trackEvent?.({
1900
+ event_name: 'product_click',
1901
+ clicked_item_id: result.id,
1902
+ metadata: { result, position: absolutePosition },
1903
+ }, searchContext);
1904
+ }
1252
1905
  }
1253
1906
  catch (err) {
1254
1907
  const error = err instanceof Error ? err : new Error(String(err));
1255
- console.error('SearchResults: Error tracking analytics event', {
1256
- resultId: result.id,
1257
- error: error.message,
1258
- });
1908
+ log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
1259
1909
  }
1260
1910
  }
1261
1911
  // Call user-provided callback
@@ -2613,7 +3263,7 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
2613
3263
  *
2614
3264
  * Renders highlighted search result text with matching terms emphasized
2615
3265
  */
2616
- const Highlight = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
3266
+ const Highlight$1 = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
2617
3267
  const { theme } = useSearchContext();
2618
3268
  const highlightTheme = customTheme || {};
2619
3269
  // Get highlighted value from hit
@@ -3914,7 +4564,7 @@ function transformFilteredTab(raw) {
3914
4564
  // Main Hook
3915
4565
  // ============================================================================
3916
4566
  function useQuerySuggestionsEnhanced(options) {
3917
- const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeCategories = false, includeFacets = false, maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
4567
+ const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeCategories = true, includeFacets = false, maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
3918
4568
  // State
3919
4569
  const [suggestions, setSuggestions] = useState([]);
3920
4570
  const [loading, setLoading] = useState(false);
@@ -5316,7 +5966,7 @@ const ImagePlaceholder = () => (React.createElement("svg", { viewBox: "0 0 24 24
5316
5966
  // Component
5317
5967
  // ============================================================================
5318
5968
  const FederatedDropdown = forwardRef(function FederatedDropdown(props, ref) {
5319
- const { query, isOpen = true, maxSuggestions = 8, maxProducts = 8, maxBrands = 8, minQueryLength = 0, debounceMs = 200, filteredTabs: tabsConfig, showProducts = true, showBrands = true, showFilteredTabs = true, showRecentSearches = true, layout = 'side-by-side', productsColumnWidth = '60%', suggestionsColumnWidth = '40%', showPrices = true, currencySymbol = '$', classNames = {}, style, renderProduct, renderSuggestion, renderBrand, renderTab, header, footer, viewAllProductsLink, width = '800px', maxHeight = '600px', zIndex = 1000, analyticsTags, onSuggestionSelect, onProductClick, onBrandClick, onTabSelect, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
5969
+ const { query, isOpen = true, maxSuggestions = 8, maxProducts = 8, maxBrands = 8, minQueryLength = 0, debounceMs = 200, filteredTabs: tabsConfig, showProducts = true, showBrands = true, showFilteredTabs = true, showRecentSearches = true, layout = 'side-by-side', productsColumnWidth = '60%', suggestionsColumnWidth = '40%', showPrices = true, currencySymbol = '$', classNames = {}, style, renderProduct, renderSuggestion, renderBrand, renderTab, header, footer, viewAllProductsLink, width = '800px', maxHeight = '600px', zIndex = 1000, analyticsTags, includeFacets: includeFacetsProp = true, includeCategories: includeCategoriesProp = true, includeDropdownRecommendations: includeDropdownRecommendationsProp = true, onSuggestionSelect, onProductClick, onBrandClick, onTabSelect, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
5320
5970
  const { client } = useSearchContext();
5321
5971
  const containerRef = useRef(null);
5322
5972
  const [activeIndex, setActiveIndex] = useState(-1);
@@ -5331,7 +5981,9 @@ const FederatedDropdown = forwardRef(function FederatedDropdown(props, ref) {
5331
5981
  debounceMs,
5332
5982
  maxSuggestions,
5333
5983
  minQueryLength,
5334
- includeDropdownRecommendations: true,
5984
+ includeDropdownRecommendations: includeDropdownRecommendationsProp,
5985
+ includeCategories: includeCategoriesProp,
5986
+ includeFacets: includeFacetsProp,
5335
5987
  filteredTabs: tabsConfig,
5336
5988
  analyticsTags,
5337
5989
  enableRecentSearches: showRecentSearches,
@@ -5587,7 +6239,7 @@ const EVENTS = {
5587
6239
  // Hook Implementation
5588
6240
  // ============================================================================
5589
6241
  function useSuggestionsAnalytics(options) {
5590
- const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, } = options;
6242
+ const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, context: contextOption, } = options;
5591
6243
  // Refs for debouncing and tracking
5592
6244
  const impressionTimerRef = useRef(null);
5593
6245
  const lastImpressionRef = useRef(null);
@@ -5600,10 +6252,11 @@ function useSuggestionsAnalytics(options) {
5600
6252
  }
5601
6253
  };
5602
6254
  }, []);
5603
- // Helper to send event
5604
- const sendEvent = useCallback(async (eventName, metadata) => {
6255
+ // Helper to send event (optional context links event to search for v3 analytics)
6256
+ const sendEvent = useCallback(async (eventName, metadata, context) => {
5605
6257
  if (!enabled || !client)
5606
6258
  return;
6259
+ const searchContext = context ?? contextOption;
5607
6260
  try {
5608
6261
  await client.trackEvent?.({
5609
6262
  event_name: eventName,
@@ -5613,13 +6266,13 @@ function useSuggestionsAnalytics(options) {
5613
6266
  timestamp: Date.now(),
5614
6267
  source: 'suggestions_dropdown',
5615
6268
  },
5616
- });
6269
+ }, searchContext);
5617
6270
  log.verbose(`Analytics: ${eventName}`, metadata);
5618
6271
  }
5619
6272
  catch (error) {
5620
6273
  log.warn(`Failed to track ${eventName}`, { error });
5621
6274
  }
5622
- }, [client, enabled, analyticsTags]);
6275
+ }, [client, enabled, analyticsTags, contextOption]);
5623
6276
  // Track suggestion click
5624
6277
  const trackSuggestionClick = useCallback((data) => {
5625
6278
  if (!trackClicks)
@@ -5647,11 +6300,12 @@ function useSuggestionsAnalytics(options) {
5647
6300
  tab_id: data.tabId,
5648
6301
  original_query: data.query,
5649
6302
  });
5650
- // Also track as a general product click for analytics
5651
- if (client) {
5652
- Promise.resolve(client.trackClick?.(data.product.id || data.product.objectID || '', data.position + 1)).catch(() => { });
6303
+ // Also track as a general product click for analytics (with context when available)
6304
+ const searchContext = contextOption;
6305
+ if (client?.trackClick) {
6306
+ Promise.resolve(client.trackClick(data.product.id || data.product.objectID || '', data.position + 1, searchContext)).catch(() => { });
5653
6307
  }
5654
- }, [client, sendEvent, trackClicks]);
6308
+ }, [client, contextOption, sendEvent, trackClicks]);
5655
6309
  // Track category click
5656
6310
  const trackCategoryClick = useCallback((category, query) => {
5657
6311
  if (!trackClicks)
@@ -5868,7 +6522,7 @@ const ClearIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill
5868
6522
  // Component
5869
6523
  // ============================================================================
5870
6524
  const SearchBarWithSuggestions = forwardRef(function SearchBarWithSuggestions(props, ref) {
5871
- const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
6525
+ const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, includeFacets, includeCategories, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
5872
6526
  const { client } = useSearchContext();
5873
6527
  const inputRef = useRef(null);
5874
6528
  const dropdownRef = useRef(null);
@@ -6027,7 +6681,7 @@ const SearchBarWithSuggestions = forwardRef(function SearchBarWithSuggestions(pr
6027
6681
  case 'rich':
6028
6682
  return (React.createElement(RichQuerySuggestions, { ref: dropdownRef, ...commonProps, includeDropdownRecommendations: includeDropdownRecommendations, includeCategories: true, width: dropdownWidth || '100%', maxHeight: dropdownMaxHeight || '480px' }));
6029
6683
  case 'federated':
6030
- return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px' }));
6684
+ return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px', includeFacets: includeFacets, includeCategories: includeCategories, includeDropdownRecommendations: includeDropdownRecommendations }));
6031
6685
  case 'compact':
6032
6686
  return (React.createElement(QuerySuggestionsDropdown, { ref: dropdownRef, ...commonProps, maxSuggestions: 5, showCounts: false, width: dropdownWidth || '100%' }));
6033
6687
  case 'classic':
@@ -10561,6 +11215,968 @@ const SuggestionDropdownVariants = {
10561
11215
  minimal: MinimalDropdown,
10562
11216
  };
10563
11217
 
11218
+ function Modal({ isOpen, onClose, children }) {
11219
+ const overlayRef = useRef(null);
11220
+ const containerRef = useRef(null);
11221
+ useEffect(() => {
11222
+ const handleClickOutside = (event) => {
11223
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
11224
+ onClose();
11225
+ }
11226
+ };
11227
+ if (isOpen)
11228
+ document.addEventListener('mousedown', handleClickOutside);
11229
+ return () => document.removeEventListener('mousedown', handleClickOutside);
11230
+ }, [isOpen, onClose]);
11231
+ useEffect(() => {
11232
+ if (isOpen) {
11233
+ const originalOverflow = document.body.style.overflow;
11234
+ document.body.style.overflow = 'hidden';
11235
+ return () => { document.body.style.overflow = originalOverflow; };
11236
+ }
11237
+ }, [isOpen]);
11238
+ useEffect(() => {
11239
+ if (!isOpen || !containerRef.current)
11240
+ return;
11241
+ const container = containerRef.current;
11242
+ const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
11243
+ const firstElement = focusableElements[0];
11244
+ const lastElement = focusableElements[focusableElements.length - 1];
11245
+ const handleTabKey = (e) => {
11246
+ if (e.key !== 'Tab')
11247
+ return;
11248
+ if (e.shiftKey) {
11249
+ if (document.activeElement === firstElement) {
11250
+ e.preventDefault();
11251
+ lastElement?.focus();
11252
+ }
11253
+ }
11254
+ else {
11255
+ if (document.activeElement === lastElement) {
11256
+ e.preventDefault();
11257
+ firstElement?.focus();
11258
+ }
11259
+ }
11260
+ };
11261
+ container.addEventListener('keydown', handleTabKey);
11262
+ return () => container.removeEventListener('keydown', handleTabKey);
11263
+ }, [isOpen]);
11264
+ if (!isOpen || typeof document === 'undefined')
11265
+ return null;
11266
+ const modalContent = (React.createElement("div", { ref: overlayRef, className: "seekora-docsearch-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Search documentation" },
11267
+ React.createElement("div", { ref: containerRef, className: "seekora-docsearch-container" }, children)));
11268
+ return createPortal(modalContent, document.body);
11269
+ }
11270
+
11271
+ function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
11272
+ const inputRef = useRef(null);
11273
+ useEffect(() => { if (inputRef.current)
11274
+ inputRef.current.focus(); }, []);
11275
+ const handleChange = (event) => onChange(event.target.value);
11276
+ const handleClear = () => { onChange(''); onClear?.(); inputRef.current?.focus(); };
11277
+ return (React.createElement("div", { className: "seekora-docsearch-searchbox" },
11278
+ React.createElement("label", { className: "seekora-docsearch-searchbox-icon", htmlFor: "seekora-docsearch-input" }, isLoading ? (React.createElement("span", { className: "seekora-docsearch-spinner", "aria-hidden": "true" },
11279
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20" },
11280
+ React.createElement("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "40", strokeDashoffset: "10" },
11281
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 10 10", to: "360 10 10", dur: "0.8s", repeatCount: "indefinite" }))))) : (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11282
+ React.createElement("path", { d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", fill: "currentColor" })))),
11283
+ React.createElement("input", { ref: inputRef, id: "seekora-docsearch-input", className: "seekora-docsearch-input", type: "text", value: value, onChange: handleChange, onKeyDown: onKeyDown, placeholder: placeholder, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-autocomplete": "list", "aria-controls": "seekora-docsearch-results" }),
11284
+ value && (React.createElement("button", { type: "button", className: "seekora-docsearch-clear", onClick: handleClear, "aria-label": "Clear search" },
11285
+ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11286
+ React.createElement("path", { d: "M4.28 3.22a.75.75 0 00-1.06 1.06L6.94 8l-3.72 3.72a.75.75 0 101.06 1.06L8 9.06l3.72 3.72a.75.75 0 101.06-1.06L9.06 8l3.72-3.72a.75.75 0 00-1.06-1.06L8 6.94 4.28 3.22z", fill: "currentColor" }))))));
11287
+ }
11288
+
11289
+ function sanitizeHtml(html) {
11290
+ const escaped = html
11291
+ .replace(/&/g, '&amp;')
11292
+ .replace(/</g, '&lt;')
11293
+ .replace(/>/g, '&gt;')
11294
+ .replace(/"/g, '&quot;')
11295
+ .replace(/'/g, '&#039;');
11296
+ return escaped
11297
+ .replace(/&lt;mark&gt;/g, '<mark>')
11298
+ .replace(/&lt;\/mark&gt;/g, '</mark>')
11299
+ .replace(/&lt;ais-highlight&gt;/g, '<mark>')
11300
+ .replace(/&lt;\/ais-highlight&gt;/g, '</mark>')
11301
+ .replace(/&lt;em&gt;/g, '<mark>')
11302
+ .replace(/&lt;\/em&gt;/g, '</mark>');
11303
+ }
11304
+ function Highlight({ value, highlightedValue }) {
11305
+ if (!highlightedValue)
11306
+ return React.createElement("span", null, value);
11307
+ return (React.createElement("span", { className: "seekora-docsearch-highlight", dangerouslySetInnerHTML: { __html: sanitizeHtml(highlightedValue) } }));
11308
+ }
11309
+ function truncateAroundMatch(content, maxLength = 150) {
11310
+ const markIndex = content.indexOf('<mark>');
11311
+ if (markIndex === -1 || content.length <= maxLength) {
11312
+ if (content.length <= maxLength)
11313
+ return content;
11314
+ return content.slice(0, maxLength) + '...';
11315
+ }
11316
+ const halfLength = Math.floor(maxLength / 2);
11317
+ let start = Math.max(0, markIndex - halfLength);
11318
+ let end = Math.min(content.length, markIndex + halfLength);
11319
+ if (start > 0) {
11320
+ const spaceIndex = content.indexOf(' ', start);
11321
+ if (spaceIndex !== -1 && spaceIndex < markIndex)
11322
+ start = spaceIndex + 1;
11323
+ }
11324
+ if (end < content.length) {
11325
+ const spaceIndex = content.lastIndexOf(' ', end);
11326
+ if (spaceIndex !== -1 && spaceIndex > markIndex)
11327
+ end = spaceIndex;
11328
+ }
11329
+ let result = content.slice(start, end);
11330
+ if (start > 0)
11331
+ result = '...' + result;
11332
+ if (end < content.length)
11333
+ result = result + '...';
11334
+ return result;
11335
+ }
11336
+
11337
+ function Hit({ hit, isSelected, onClick, onMouseEnter, openInNewTab, isChild, isLastChild, hierarchyType }) {
11338
+ const isFullHit = 'objectID' in hit;
11339
+ const suggestion = hit;
11340
+ const hitType = hierarchyType || suggestion.type;
11341
+ const breadcrumb = suggestion.parentTitle
11342
+ ? `${suggestion.category || ''} › ${suggestion.parentTitle}`.replace(/^› /, '')
11343
+ : suggestion.category || '';
11344
+ const title = getTitleForType(hit, hitType);
11345
+ let highlightedTitle = title;
11346
+ let highlightedContent = hit.content || suggestion.description || '';
11347
+ if (isFullHit) {
11348
+ const fullHit = hit;
11349
+ if (fullHit._highlightResult) {
11350
+ highlightedTitle = fullHit._highlightResult.title?.value || title;
11351
+ highlightedContent = fullHit._highlightResult.content?.value || hit.content || '';
11352
+ }
11353
+ }
11354
+ else {
11355
+ if (suggestion.highlight) {
11356
+ highlightedTitle = suggestion.highlight.title || title;
11357
+ highlightedContent = suggestion.highlight.content || hit.content || suggestion.description || '';
11358
+ }
11359
+ }
11360
+ const displayContent = highlightedContent ? truncateAroundMatch(highlightedContent, 120) : '';
11361
+ const url = hit.url || suggestion.route || '#';
11362
+ const classNames = ['seekora-docsearch-hit'];
11363
+ if (isSelected)
11364
+ classNames.push('seekora-docsearch-hit--selected');
11365
+ if (isChild)
11366
+ classNames.push('seekora-docsearch-hit--child');
11367
+ if (isLastChild)
11368
+ classNames.push('seekora-docsearch-hit--last-child');
11369
+ return (React.createElement("a", { href: url, className: classNames.join(' '), onClick: (e) => { e.preventDefault(); onClick(); }, onMouseEnter: onMouseEnter, role: "option", "aria-selected": isSelected, target: openInNewTab ? '_blank' : undefined, rel: openInNewTab ? 'noopener noreferrer' : undefined },
11370
+ isChild && (React.createElement("div", { className: "seekora-docsearch-hit-tree" },
11371
+ React.createElement(TreeConnector, { isLast: isLastChild }))),
11372
+ React.createElement("div", { className: "seekora-docsearch-hit-icon" },
11373
+ React.createElement(HitIcon, { type: getHitTypeFromLevel(hitType) })),
11374
+ React.createElement("div", { className: "seekora-docsearch-hit-content" },
11375
+ !isChild && breadcrumb && React.createElement("span", { className: "seekora-docsearch-hit-breadcrumb" }, breadcrumb),
11376
+ React.createElement("span", { className: "seekora-docsearch-hit-title" },
11377
+ React.createElement(Highlight, { value: title, highlightedValue: highlightedTitle })),
11378
+ displayContent && (React.createElement("span", { className: "seekora-docsearch-hit-description" },
11379
+ React.createElement(Highlight, { value: hit.content || '', highlightedValue: displayContent })))),
11380
+ React.createElement("div", { className: "seekora-docsearch-hit-action" }, openInNewTab ? (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
11381
+ React.createElement("path", { d: "M6 3H3v10h10v-3M9 3h4v4M14 2L7 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))) : (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
11382
+ React.createElement("path", { d: "M6.75 3.25L11.5 8L6.75 12.75", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))))));
11383
+ }
11384
+ function getTitleForType(hit, type) {
11385
+ const hierarchy = hit.hierarchy || {};
11386
+ if (!type)
11387
+ return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
11388
+ const match = type.match(/^lvl(\d+)$/);
11389
+ if (match) {
11390
+ const level = parseInt(match[1], 10);
11391
+ const levelKey = `lvl${level}`;
11392
+ const levelTitle = hierarchy[levelKey];
11393
+ if (levelTitle)
11394
+ return levelTitle;
11395
+ }
11396
+ return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
11397
+ }
11398
+ function getHitTypeFromLevel(type) {
11399
+ if (!type)
11400
+ return 'page';
11401
+ const match = type.match(/^lvl(\d+)$/);
11402
+ if (match) {
11403
+ const level = parseInt(match[1], 10);
11404
+ if (level === 1)
11405
+ return 'page';
11406
+ if (level <= 3)
11407
+ return 'section';
11408
+ return 'content';
11409
+ }
11410
+ return 'page';
11411
+ }
11412
+ function HitIcon({ type }) {
11413
+ switch (type) {
11414
+ case 'page':
11415
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11416
+ React.createElement("path", { d: "M4.5 3.5h11a1 1 0 011 1v11a1 1 0 01-1 1h-11a1 1 0 01-1-1v-11a1 1 0 011-1z", stroke: "currentColor", strokeWidth: "1.5", fill: "none" }),
11417
+ React.createElement("path", { d: "M7 7h6M7 10h6M7 13h4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11418
+ case 'section':
11419
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11420
+ React.createElement("path", { d: "M4 5.5h12M4 10h12M4 14.5h8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11421
+ case 'content':
11422
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11423
+ React.createElement("path", { d: "M4 6h12M4 10h8M4 14h10", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11424
+ }
11425
+ }
11426
+ function TreeConnector({ isLast }) {
11427
+ return (React.createElement("svg", { width: "16", height: "20", viewBox: "0 0 16 20", fill: "none", "aria-hidden": "true", className: "seekora-docsearch-hit-tree-icon" }, isLast ? (React.createElement("path", { d: "M8 0V10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })) : (React.createElement(React.Fragment, null,
11428
+ React.createElement("path", { d: "M8 0V20", stroke: "currentColor", strokeWidth: "1.5" }),
11429
+ React.createElement("path", { d: "M8 10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })))));
11430
+ }
11431
+
11432
+ function getTypeLevel(type) {
11433
+ if (!type)
11434
+ return 1;
11435
+ const match = type.match(/^lvl(\d+)$/);
11436
+ return match ? parseInt(match[1], 10) : 1;
11437
+ }
11438
+ function isChildType(type) {
11439
+ return getTypeLevel(type) >= 2;
11440
+ }
11441
+ /** Build a stable grouping key from all hierarchy levels (lvl0 through lvl5). */
11442
+ function getHierarchyKey(hit) {
11443
+ const h = hit.hierarchy ?? {};
11444
+ const parts = [
11445
+ h.lvl0 ?? '',
11446
+ h.lvl1 ?? '',
11447
+ h.lvl2 ?? '',
11448
+ h.lvl3 ?? '',
11449
+ h.lvl4 ?? '',
11450
+ h.lvl5 ?? '',
11451
+ ];
11452
+ return parts.join('\0');
11453
+ }
11454
+ /** Build a display breadcrumb from hierarchy, skipping consecutive duplicates (e.g. lvl0 === lvl1). */
11455
+ function getHierarchyBreadcrumb(hit) {
11456
+ const h = hit.hierarchy ?? {};
11457
+ const parts = [h.lvl0, h.lvl1, h.lvl2, h.lvl3, h.lvl4, h.lvl5].filter((v) => typeof v === 'string' && v.length > 0);
11458
+ const deduped = [];
11459
+ for (const p of parts) {
11460
+ if (deduped[deduped.length - 1] !== p)
11461
+ deduped.push(p);
11462
+ }
11463
+ return deduped.join(' › ');
11464
+ }
11465
+ function groupHitsByHierarchy(hits) {
11466
+ const hitToIndex = new Map();
11467
+ hits.forEach((h, i) => hitToIndex.set(h, i));
11468
+ const groups = new Map();
11469
+ for (const hit of hits) {
11470
+ const key = getHierarchyKey(hit);
11471
+ if (!groups.has(key))
11472
+ groups.set(key, []);
11473
+ groups.get(key).push(hit);
11474
+ }
11475
+ const entries = Array.from(groups.entries());
11476
+ entries.sort(([, aHits], [, bHits]) => {
11477
+ const aMin = Math.min(...aHits.map(h => hitToIndex.get(h) ?? 0));
11478
+ const bMin = Math.min(...bHits.map(h => hitToIndex.get(h) ?? 0));
11479
+ return aMin - bMin;
11480
+ });
11481
+ const result = [];
11482
+ for (const [, groupHits] of entries) {
11483
+ const sortedHits = [...groupHits].sort((a, b) => {
11484
+ const aType = a.type;
11485
+ const bType = b.type;
11486
+ return getTypeLevel(aType) - getTypeLevel(bType);
11487
+ });
11488
+ const markedHits = sortedHits.map((hit, index) => {
11489
+ const suggestion = hit;
11490
+ const isChild = isChildType(suggestion.type);
11491
+ const nextHit = sortedHits[index + 1];
11492
+ const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
11493
+ return { ...hit, isChild, isLastChild };
11494
+ });
11495
+ const category = getHierarchyBreadcrumb(groupHits[0]);
11496
+ result.push({ category: category || null, hits: markedHits });
11497
+ }
11498
+ return result;
11499
+ }
11500
+ function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
11501
+ let index = 0;
11502
+ for (let i = 0; i < groupIndex; i++)
11503
+ index += groups[i].hits.length;
11504
+ return index + hitIndex;
11505
+ }
11506
+ function getHitKey(hit, index) {
11507
+ if ('objectID' in hit)
11508
+ return hit.objectID;
11509
+ return `suggestion-${hit.url}-${index}`;
11510
+ }
11511
+ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
11512
+ const listRef = useRef(null);
11513
+ useEffect(() => {
11514
+ if (!listRef.current || hits.length === 0)
11515
+ return;
11516
+ // Only scroll when selection changed via keyboard (avoids scroll-to-selected on hover)
11517
+ if (scrollSelectionIntoViewRef && !scrollSelectionIntoViewRef.current)
11518
+ return;
11519
+ // listRef's direct children are groups (li.seekora-docsearch-results-group), not hits.
11520
+ // Find the actual hit element at flat selectedIndex.
11521
+ const groupEls = listRef.current.querySelectorAll('.seekora-docsearch-results-group');
11522
+ let idx = 0;
11523
+ for (const groupEl of groupEls) {
11524
+ const itemList = groupEl.querySelector('.seekora-docsearch-results-group-items');
11525
+ if (!itemList)
11526
+ continue;
11527
+ const items = itemList.children;
11528
+ for (let i = 0; i < items.length; i++) {
11529
+ if (idx === selectedIndex) {
11530
+ items[i].scrollIntoView({ block: 'nearest' });
11531
+ if (scrollSelectionIntoViewRef)
11532
+ scrollSelectionIntoViewRef.current = false;
11533
+ return;
11534
+ }
11535
+ idx++;
11536
+ }
11537
+ }
11538
+ if (scrollSelectionIntoViewRef)
11539
+ scrollSelectionIntoViewRef.current = false;
11540
+ }, [selectedIndex, hits.length, scrollSelectionIntoViewRef]);
11541
+ if (!query) {
11542
+ return (React.createElement("div", { className: "seekora-docsearch-empty" },
11543
+ React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
11544
+ }
11545
+ if (isLoading && hits.length === 0) {
11546
+ return (React.createElement("div", { className: "seekora-docsearch-loading" },
11547
+ React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
11548
+ React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
11549
+ React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
11550
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
11551
+ React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
11552
+ }
11553
+ if (error) {
11554
+ return (React.createElement("div", { className: "seekora-docsearch-error" },
11555
+ React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
11556
+ }
11557
+ if (hits.length === 0 && query) {
11558
+ return (React.createElement("div", { className: "seekora-docsearch-no-results" },
11559
+ React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
11560
+ }
11561
+ const displayGroups = groupedHits && groupedHits.length > 0
11562
+ ? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
11563
+ : groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
11564
+ return (React.createElement("div", { className: "seekora-docsearch-results" },
11565
+ React.createElement("ul", { ref: listRef, id: "seekora-docsearch-results", className: "seekora-docsearch-results-list", role: "listbox" }, displayGroups.map((group, groupIndex) => (React.createElement("li", { key: group.sourceId + '-' + groupIndex, className: "seekora-docsearch-results-group" },
11566
+ group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
11567
+ React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
11568
+ const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
11569
+ const extHit = hit;
11570
+ return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
11571
+ React.createElement(Hit, { hit: hit, isSelected: globalIndex === selectedIndex, onClick: () => onSelect(hit), onMouseEnter: () => onHover(globalIndex), openInNewTab: group.openInNewTab, isChild: extHit.isChild, isLastChild: extHit.isLastChild, hierarchyType: hit.type })));
11572
+ }))))))));
11573
+ }
11574
+
11575
+ function Footer({ translations = {} }) {
11576
+ return (React.createElement("footer", { className: "seekora-docsearch-footer" },
11577
+ React.createElement("div", { className: "seekora-docsearch-footer-commands" },
11578
+ React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
11579
+ React.createElement("li", null,
11580
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11581
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
11582
+ React.createElement("span", null, "to select"))),
11583
+ React.createElement("li", null,
11584
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11585
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
11586
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
11587
+ React.createElement("span", null, "to navigate"))),
11588
+ React.createElement("li", null,
11589
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11590
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
11591
+ React.createElement("span", null, translations.closeText || 'to close'))))),
11592
+ React.createElement("div", { className: "seekora-docsearch-footer-logo" },
11593
+ React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
11594
+ React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
11595
+ React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
11596
+ }
11597
+
11598
+ function useKeyboard(options) {
11599
+ const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
11600
+ const handleGlobalKeyDown = useCallback((event) => {
11601
+ if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
11602
+ event.preventDefault();
11603
+ if (isOpen) {
11604
+ onClose();
11605
+ }
11606
+ else {
11607
+ onOpen();
11608
+ }
11609
+ return;
11610
+ }
11611
+ if (!disableShortcut && event.key === '/' && !isOpen) {
11612
+ const target = event.target;
11613
+ const isInput = target.tagName === 'INPUT' ||
11614
+ target.tagName === 'TEXTAREA' ||
11615
+ target.isContentEditable;
11616
+ if (!isInput) {
11617
+ event.preventDefault();
11618
+ onOpen();
11619
+ }
11620
+ }
11621
+ }, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
11622
+ const handleModalKeyDown = useCallback((event) => {
11623
+ switch (event.key) {
11624
+ case 'Escape':
11625
+ event.preventDefault();
11626
+ onClose();
11627
+ break;
11628
+ case 'ArrowDown':
11629
+ event.preventDefault();
11630
+ onSelectNext();
11631
+ break;
11632
+ case 'ArrowUp':
11633
+ event.preventDefault();
11634
+ onSelectPrev();
11635
+ break;
11636
+ case 'Enter':
11637
+ event.preventDefault();
11638
+ onEnter();
11639
+ break;
11640
+ case 'Tab':
11641
+ if (event.shiftKey) {
11642
+ onSelectPrev();
11643
+ }
11644
+ else {
11645
+ onSelectNext();
11646
+ }
11647
+ event.preventDefault();
11648
+ break;
11649
+ }
11650
+ }, [onClose, onSelectNext, onSelectPrev, onEnter]);
11651
+ useEffect(() => {
11652
+ document.addEventListener('keydown', handleGlobalKeyDown);
11653
+ return () => {
11654
+ document.removeEventListener('keydown', handleGlobalKeyDown);
11655
+ };
11656
+ }, [handleGlobalKeyDown]);
11657
+ return {
11658
+ handleModalKeyDown,
11659
+ };
11660
+ }
11661
+ function getShortcutText(key = 'K') {
11662
+ if (typeof navigator === 'undefined') {
11663
+ return `⌘${key}`;
11664
+ }
11665
+ const isMac = navigator.platform.toLowerCase().includes('mac');
11666
+ return isMac ? `⌘${key}` : `Ctrl+${key}`;
11667
+ }
11668
+
11669
+ function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
11670
+ const shortcutText = getShortcutText('K');
11671
+ return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
11672
+ React.createElement("span", { className: "seekora-docsearch-button-icon" },
11673
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11674
+ React.createElement("path", { d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", fill: "currentColor" }))),
11675
+ React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
11676
+ React.createElement("span", { className: "seekora-docsearch-button-keys" },
11677
+ React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
11678
+ }
11679
+
11680
+ const initialState = {
11681
+ query: '',
11682
+ results: [],
11683
+ suggestions: [],
11684
+ groupedSuggestions: [],
11685
+ isLoading: false,
11686
+ error: null,
11687
+ selectedIndex: 0,
11688
+ mode: 'suggestions',
11689
+ };
11690
+ function reducer(state, action) {
11691
+ switch (action.type) {
11692
+ case 'SET_QUERY':
11693
+ return { ...state, query: action.payload, selectedIndex: 0 };
11694
+ case 'SET_RESULTS':
11695
+ return { ...state, results: action.payload, mode: 'results' };
11696
+ case 'SET_SUGGESTIONS':
11697
+ return { ...state, suggestions: action.payload, mode: 'suggestions' };
11698
+ case 'SET_GROUPED_SUGGESTIONS': {
11699
+ const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
11700
+ return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
11701
+ }
11702
+ case 'SET_LOADING':
11703
+ return { ...state, isLoading: action.payload };
11704
+ case 'SET_ERROR':
11705
+ return { ...state, error: action.payload };
11706
+ case 'SET_SELECTED_INDEX':
11707
+ return { ...state, selectedIndex: action.payload };
11708
+ case 'SELECT_NEXT': {
11709
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11710
+ const maxIndex = items.length - 1;
11711
+ return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
11712
+ }
11713
+ case 'SELECT_PREV': {
11714
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11715
+ const maxIndex = items.length - 1;
11716
+ return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
11717
+ }
11718
+ case 'SET_MODE':
11719
+ return { ...state, mode: action.payload, selectedIndex: 0 };
11720
+ case 'RESET':
11721
+ return initialState;
11722
+ default:
11723
+ return state;
11724
+ }
11725
+ }
11726
+ function useDocSearch(options) {
11727
+ const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200, processGroupedResults } = options;
11728
+ const searchSources = sources || (apiEndpoint ? [{
11729
+ id: 'default',
11730
+ name: 'Results',
11731
+ endpoint: apiEndpoint,
11732
+ apiKey,
11733
+ maxResults,
11734
+ }] : []);
11735
+ const [state, dispatch] = useReducer(reducer, initialState);
11736
+ const abortControllersRef = useRef(new Map());
11737
+ const debounceTimerRef = useRef(null);
11738
+ const defaultTransform = (data, sourceId) => {
11739
+ const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
11740
+ return items.map((item) => ({
11741
+ url: item.url || item.route || '',
11742
+ title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
11743
+ content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
11744
+ description: item.description || item.content?.substring?.(0, 100) || '',
11745
+ category: item.category || item.hierarchy?.lvl0 || '',
11746
+ hierarchy: item.hierarchy,
11747
+ route: item.route,
11748
+ parentTitle: item.parent_title || item.parentTitle,
11749
+ _source: sourceId,
11750
+ }));
11751
+ };
11752
+ const transformPublicSearchResults = useCallback((data, sourceId) => {
11753
+ const results = data?.data?.results || [];
11754
+ return results.map((item) => {
11755
+ const doc = item.document || item;
11756
+ return {
11757
+ url: doc.url || doc.route || '',
11758
+ title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
11759
+ content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
11760
+ description: doc.description || doc.content?.substring?.(0, 100) || '',
11761
+ category: doc.category || doc.hierarchy?.lvl0 || '',
11762
+ hierarchy: doc.hierarchy,
11763
+ route: doc.route,
11764
+ parentTitle: doc.parent_title || doc.parentTitle,
11765
+ _source: sourceId,
11766
+ };
11767
+ });
11768
+ }, []);
11769
+ const fetchFromSource = useCallback(async (source, query, signal) => {
11770
+ const minLength = source.minQueryLength ?? 1;
11771
+ if (query.length < minLength)
11772
+ return [];
11773
+ try {
11774
+ if (source.storeId) {
11775
+ const baseUrl = source.endpoint.replace(/\/$/, '');
11776
+ const searchUrl = `${baseUrl}/api/v1/search`;
11777
+ const headers = {
11778
+ 'Content-Type': 'application/json',
11779
+ 'x-storeid': source.storeId,
11780
+ ...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
11781
+ };
11782
+ const response = await fetch(searchUrl, {
11783
+ method: 'POST',
11784
+ headers,
11785
+ body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
11786
+ signal,
11787
+ });
11788
+ if (!response.ok)
11789
+ return [];
11790
+ const data = await response.json();
11791
+ if (source.transformResults) {
11792
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11793
+ }
11794
+ return transformPublicSearchResults(data, source.id);
11795
+ }
11796
+ const url = new URL(source.endpoint);
11797
+ url.searchParams.set('query', query);
11798
+ url.searchParams.set('limit', String(source.maxResults || 8));
11799
+ const headers = { 'Content-Type': 'application/json' };
11800
+ if (source.apiKey)
11801
+ headers['X-Docs-API-Key'] = source.apiKey;
11802
+ const response = await fetch(url.toString(), { method: 'GET', headers, signal });
11803
+ if (!response.ok)
11804
+ return [];
11805
+ const data = await response.json();
11806
+ if (source.transformResults) {
11807
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11808
+ }
11809
+ return defaultTransform(data, source.id);
11810
+ }
11811
+ catch (error) {
11812
+ if (error instanceof Error && error.name === 'AbortError')
11813
+ throw error;
11814
+ return [];
11815
+ }
11816
+ }, [transformPublicSearchResults]);
11817
+ const fetchSuggestions = useCallback(async (query) => {
11818
+ if (!query.trim()) {
11819
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
11820
+ return;
11821
+ }
11822
+ abortControllersRef.current.forEach(c => c.abort());
11823
+ abortControllersRef.current.clear();
11824
+ dispatch({ type: 'SET_LOADING', payload: true });
11825
+ dispatch({ type: 'SET_ERROR', payload: null });
11826
+ try {
11827
+ const results = await Promise.all(searchSources.map(async (source) => {
11828
+ const controller = new AbortController();
11829
+ abortControllersRef.current.set(source.id, controller);
11830
+ let items = await fetchFromSource(source, query, controller.signal);
11831
+ if (processGroupedResults) {
11832
+ items = processGroupedResults(source.id, items);
11833
+ }
11834
+ return { source, items };
11835
+ }));
11836
+ const groupedResults = results.filter(r => r.items.length > 0);
11837
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
11838
+ }
11839
+ catch (error) {
11840
+ if (error instanceof Error && error.name === 'AbortError')
11841
+ return;
11842
+ dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
11843
+ }
11844
+ finally {
11845
+ dispatch({ type: 'SET_LOADING', payload: false });
11846
+ }
11847
+ }, [searchSources, fetchFromSource, processGroupedResults]);
11848
+ const search = useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
11849
+ const setQuery = useCallback((query) => {
11850
+ dispatch({ type: 'SET_QUERY', payload: query });
11851
+ if (debounceTimerRef.current)
11852
+ clearTimeout(debounceTimerRef.current);
11853
+ debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
11854
+ }, [fetchSuggestions, debounceMs]);
11855
+ const selectNext = useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
11856
+ const selectPrev = useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
11857
+ const setSelectedIndex = useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
11858
+ const reset = useCallback(() => {
11859
+ abortControllersRef.current.forEach(c => c.abort());
11860
+ abortControllersRef.current.clear();
11861
+ if (debounceTimerRef.current)
11862
+ clearTimeout(debounceTimerRef.current);
11863
+ dispatch({ type: 'RESET' });
11864
+ }, []);
11865
+ const getSelectedItem = useCallback(() => {
11866
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11867
+ return items[state.selectedIndex] || null;
11868
+ }, [state.mode, state.results, state.suggestions, state.selectedIndex]);
11869
+ useEffect(() => {
11870
+ return () => {
11871
+ abortControllersRef.current.forEach(c => c.abort());
11872
+ abortControllersRef.current.clear();
11873
+ if (debounceTimerRef.current)
11874
+ clearTimeout(debounceTimerRef.current);
11875
+ };
11876
+ }, []);
11877
+ return {
11878
+ ...state,
11879
+ sources: searchSources,
11880
+ setQuery,
11881
+ search,
11882
+ fetchSuggestions,
11883
+ selectNext,
11884
+ selectPrev,
11885
+ setSelectedIndex,
11886
+ reset,
11887
+ getSelectedItem,
11888
+ };
11889
+ }
11890
+
11891
+ /** Build hierarchy from doc: either nested doc.hierarchy or flat keys like doc['hierarchy.lvl0'] */
11892
+ function getHierarchy(doc) {
11893
+ const hierarchy = {};
11894
+ if (doc?.hierarchy && typeof doc.hierarchy === 'object') {
11895
+ hierarchy.lvl0 = doc.hierarchy.lvl0;
11896
+ hierarchy.lvl1 = doc.hierarchy.lvl1;
11897
+ hierarchy.lvl2 = doc.hierarchy.lvl2;
11898
+ hierarchy.lvl3 = doc.hierarchy.lvl3;
11899
+ hierarchy.lvl4 = doc.hierarchy.lvl4;
11900
+ hierarchy.lvl5 = doc.hierarchy.lvl5;
11901
+ }
11902
+ else if (doc && typeof doc === 'object') {
11903
+ hierarchy.lvl0 = doc['hierarchy.lvl0'];
11904
+ hierarchy.lvl1 = doc['hierarchy.lvl1'];
11905
+ hierarchy.lvl2 = doc['hierarchy.lvl2'];
11906
+ hierarchy.lvl3 = doc['hierarchy.lvl3'];
11907
+ hierarchy.lvl4 = doc['hierarchy.lvl4'];
11908
+ hierarchy.lvl5 = doc['hierarchy.lvl5'];
11909
+ }
11910
+ return hierarchy;
11911
+ }
11912
+ function transformResults(results) {
11913
+ const stripMark = (s) => typeof s === 'string' ? s.replace(/<\/?mark>/g, '') : (s ? String(s) : '');
11914
+ return results.map((result) => {
11915
+ const doc = result?.document ?? result;
11916
+ const hierarchy = getHierarchy(doc);
11917
+ const url = doc.url || result.url || doc.route || result.route || doc.link || result.link || '';
11918
+ const rawTitle = doc.title || result.title || doc.name || result.name || hierarchy?.lvl3 || hierarchy?.lvl2 || hierarchy?.lvl1 || hierarchy?.lvl0 || '';
11919
+ const content = doc.content ?? result.content ?? doc.description ?? result.description ?? doc.snippet ?? result.snippet ?? '';
11920
+ const description = doc.description ?? result.description ?? (typeof content === 'string' ? content.substring(0, 150) : '');
11921
+ const plainTitle = stripMark(rawTitle) || stripMark(hierarchy?.lvl3) || stripMark(hierarchy?.lvl2) || stripMark(hierarchy?.lvl1) || stripMark(hierarchy?.lvl0) || 'Untitled';
11922
+ const plainContent = stripMark(content)?.substring(0, 200) || content;
11923
+ const apiHighlight = result?.highlight ?? doc?.highlight;
11924
+ const highlightTitle = typeof apiHighlight?.title === 'string' ? apiHighlight.title : undefined;
11925
+ const highlightContent = typeof apiHighlight?.content === 'string' ? apiHighlight.content : undefined;
11926
+ return {
11927
+ url,
11928
+ title: plainTitle,
11929
+ content: plainContent,
11930
+ description: stripMark(description) || description,
11931
+ category: doc.category ?? result.category ?? hierarchy?.lvl0 ?? '',
11932
+ hierarchy,
11933
+ route: doc.route ?? result.route,
11934
+ parentTitle: doc.parent_title ?? doc.parentTitle ?? result.parent_title ?? result.parentTitle,
11935
+ type: doc.type ?? result.type ?? '',
11936
+ anchor: doc.anchor ?? result.anchor ?? '',
11937
+ _source: 'seekora',
11938
+ highlight: (highlightTitle ?? highlightContent) ? { title: highlightTitle, content: highlightContent } : undefined,
11939
+ };
11940
+ });
11941
+ }
11942
+ function useSeekoraSearch$1(options) {
11943
+ const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField = 'hierarchy.lvl3', groupSize = 10, } = options;
11944
+ const [query, setQueryState] = useState('');
11945
+ const [suggestions, setSuggestions] = useState([]);
11946
+ const [isLoading, setIsLoading] = useState(false);
11947
+ const [error, setError] = useState(null);
11948
+ const [selectedIndex, setSelectedIndex] = useState(0);
11949
+ const clientRef = useRef(null);
11950
+ const lastSearchContextRef = useRef(null);
11951
+ const debounceTimerRef = useRef(null);
11952
+ const abortControllerRef = useRef(null);
11953
+ useEffect(() => {
11954
+ if (!storeId)
11955
+ return;
11956
+ const config = {
11957
+ storeId,
11958
+ readSecret: storeSecret,
11959
+ logLevel: 'warn',
11960
+ enableContextCollection: true,
11961
+ };
11962
+ if (apiEndpoint) {
11963
+ if (['local', 'stage', 'production'].includes(apiEndpoint)) {
11964
+ config.environment = apiEndpoint;
11965
+ }
11966
+ else {
11967
+ // Search SDK path is /v1/search; backend serves at /api/v1/search. baseUrl must include /api.
11968
+ const base = apiEndpoint.replace(/\/$/, '');
11969
+ config.baseUrl = base.endsWith('/api') ? base : `${base}/api`;
11970
+ }
11971
+ }
11972
+ try {
11973
+ clientRef.current = new SeekoraClient(config);
11974
+ }
11975
+ catch (err) {
11976
+ console.error('Failed to initialize SeekoraClient:', err);
11977
+ setError('Failed to initialize search client');
11978
+ }
11979
+ return () => {
11980
+ clientRef.current = null;
11981
+ };
11982
+ }, [storeId, storeSecret, apiEndpoint]);
11983
+ const performSearch = useCallback(async (searchQuery) => {
11984
+ if (!clientRef.current) {
11985
+ setError('Search client not initialized');
11986
+ return;
11987
+ }
11988
+ if (!searchQuery.trim()) {
11989
+ setSuggestions([]);
11990
+ return;
11991
+ }
11992
+ if (abortControllerRef.current)
11993
+ abortControllerRef.current.abort();
11994
+ abortControllerRef.current = new AbortController();
11995
+ setIsLoading(true);
11996
+ setError(null);
11997
+ try {
11998
+ const response = await clientRef.current.search(searchQuery, {
11999
+ per_page: maxResults,
12000
+ analytics_tags: analyticsTags,
12001
+ return_fields: [
12002
+ 'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
12003
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
12004
+ 'content', 'type', 'url', 'title', 'anchor'
12005
+ ],
12006
+ snippet_fields: [
12007
+ 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
12008
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
12009
+ ],
12010
+ snippet_prefix: '<mark>',
12011
+ snippet_suffix: '</mark>',
12012
+ include_snippets: true,
12013
+ group_field: groupField,
12014
+ group_size: groupSize,
12015
+ });
12016
+ if (abortControllerRef.current?.signal.aborted)
12017
+ return;
12018
+ if (response?.context)
12019
+ lastSearchContextRef.current = response.context;
12020
+ setSuggestions(transformResults(response.results || []));
12021
+ setSelectedIndex(0);
12022
+ }
12023
+ catch (err) {
12024
+ if (err instanceof Error && err.name === 'AbortError')
12025
+ return;
12026
+ console.error('Search failed:', err);
12027
+ setError(err instanceof Error ? err.message : 'Search failed');
12028
+ setSuggestions([]);
12029
+ }
12030
+ finally {
12031
+ setIsLoading(false);
12032
+ }
12033
+ }, [maxResults, analyticsTags, groupField, groupSize]);
12034
+ const setQuery = useCallback((newQuery) => {
12035
+ setQueryState(newQuery);
12036
+ setSelectedIndex(0);
12037
+ if (debounceTimerRef.current)
12038
+ clearTimeout(debounceTimerRef.current);
12039
+ debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
12040
+ }, [performSearch, debounceMs]);
12041
+ const selectNext = useCallback(() => {
12042
+ setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
12043
+ }, [suggestions.length]);
12044
+ const selectPrev = useCallback(() => {
12045
+ setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
12046
+ }, [suggestions.length]);
12047
+ const reset = useCallback(() => {
12048
+ if (abortControllerRef.current)
12049
+ abortControllerRef.current.abort();
12050
+ if (debounceTimerRef.current)
12051
+ clearTimeout(debounceTimerRef.current);
12052
+ setQueryState('');
12053
+ setSuggestions([]);
12054
+ setIsLoading(false);
12055
+ setError(null);
12056
+ setSelectedIndex(0);
12057
+ }, []);
12058
+ const getSelectedItem = useCallback(() => {
12059
+ return suggestions[selectedIndex] || null;
12060
+ }, [suggestions, selectedIndex]);
12061
+ const trackDocClick = useCallback((hit, position) => {
12062
+ const client = clientRef.current;
12063
+ if (!client?.trackEvent)
12064
+ return;
12065
+ const context = lastSearchContextRef.current ?? undefined;
12066
+ const itemId = hit.url || hit.id || hit.title || String(position);
12067
+ client.trackEvent({
12068
+ event_name: 'doc_click',
12069
+ clicked_item_id: itemId,
12070
+ metadata: { position, result: hit, source: 'docsearch' },
12071
+ }, context);
12072
+ }, []);
12073
+ useEffect(() => {
12074
+ return () => {
12075
+ if (abortControllerRef.current)
12076
+ abortControllerRef.current.abort();
12077
+ if (debounceTimerRef.current)
12078
+ clearTimeout(debounceTimerRef.current);
12079
+ };
12080
+ }, []);
12081
+ return {
12082
+ query,
12083
+ suggestions,
12084
+ isLoading,
12085
+ error,
12086
+ selectedIndex,
12087
+ setQuery,
12088
+ selectNext,
12089
+ selectPrev,
12090
+ setSelectedIndex,
12091
+ reset,
12092
+ getSelectedItem,
12093
+ trackDocClick,
12094
+ };
12095
+ }
12096
+
12097
+ function DocSearch({ storeId, storeSecret, seekoraApiEndpoint, apiEndpoint, apiKey, sources, placeholder = 'Search documentation...', maxResults = 10, debounceMs = 200, onSelect, onClose, translations = {}, renderButton = true, buttonComponent: ButtonComponent = DocSearchButton, initialOpen = false, disableShortcut = false, shortcutKey = 'k', processGroupedResults, }) {
12098
+ const [isOpen, setIsOpen] = useState(initialOpen);
12099
+ const scrollSelectionIntoViewRef = useRef(false);
12100
+ const useSeekoraSDK = !!storeId;
12101
+ const seekoraSearch = useSeekoraSearch$1({
12102
+ storeId: storeId || '',
12103
+ storeSecret,
12104
+ apiEndpoint: seekoraApiEndpoint,
12105
+ maxResults,
12106
+ debounceMs,
12107
+ analyticsTags: ['docsearch'],
12108
+ });
12109
+ const legacySearch = useDocSearch({
12110
+ apiEndpoint,
12111
+ apiKey,
12112
+ sources,
12113
+ maxResults,
12114
+ debounceMs,
12115
+ processGroupedResults,
12116
+ });
12117
+ const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
12118
+ const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
12119
+ const results = useSeekoraSDK ? suggestions : legacySearch.results;
12120
+ const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
12121
+ const searchSources = useSeekoraSDK
12122
+ ? [{ id: 'seekora', name: 'Results', endpoint: '' }]
12123
+ : legacySearch.sources;
12124
+ const handleOpen = useCallback(() => setIsOpen(true), []);
12125
+ const handleClose = useCallback(() => {
12126
+ setIsOpen(false);
12127
+ reset();
12128
+ onClose?.();
12129
+ }, [reset, onClose]);
12130
+ const handleSelect = useCallback((hit) => {
12131
+ if (useSeekoraSDK && seekoraSearch.trackDocClick) {
12132
+ seekoraSearch.trackDocClick(hit, selectedIndex + 1);
12133
+ }
12134
+ if (onSelect) {
12135
+ onSelect(hit);
12136
+ }
12137
+ else {
12138
+ window.location.href = hit.url;
12139
+ }
12140
+ handleClose();
12141
+ }, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
12142
+ const handleEnter = useCallback(() => {
12143
+ const selectedItem = getSelectedItem();
12144
+ if (selectedItem)
12145
+ handleSelect(selectedItem);
12146
+ }, [getSelectedItem, handleSelect]);
12147
+ const handleSelectNext = useCallback(() => {
12148
+ scrollSelectionIntoViewRef.current = true;
12149
+ selectNext();
12150
+ }, [selectNext]);
12151
+ const handleSelectPrev = useCallback(() => {
12152
+ scrollSelectionIntoViewRef.current = true;
12153
+ selectPrev();
12154
+ }, [selectPrev]);
12155
+ const { handleModalKeyDown } = useKeyboard({
12156
+ isOpen,
12157
+ onOpen: handleOpen,
12158
+ onClose: handleClose,
12159
+ onSelectNext: handleSelectNext,
12160
+ onSelectPrev: handleSelectPrev,
12161
+ onEnter: handleEnter,
12162
+ disableShortcut,
12163
+ shortcutKey,
12164
+ });
12165
+ const handleKeyDown = useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
12166
+ const displayHits = mode === 'results' ? results : suggestions;
12167
+ return (React.createElement(React.Fragment, null,
12168
+ renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
12169
+ React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
12170
+ React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
12171
+ React.createElement("header", { className: "seekora-docsearch-header" },
12172
+ React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
12173
+ React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
12174
+ React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
12175
+ React.createElement("div", { className: "seekora-docsearch-body" },
12176
+ React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, scrollSelectionIntoViewRef: scrollSelectionIntoViewRef, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
12177
+ React.createElement(Footer, { translations: translations })))));
12178
+ }
12179
+
10564
12180
  /**
10565
12181
  * useSeekoraSearch Hook
10566
12182
  *
@@ -10626,7 +12242,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
10626
12242
  /**
10627
12243
  * useAnalytics Hook
10628
12244
  *
10629
- * Hook for tracking analytics events with the Seekora SDK
12245
+ * Hook for tracking analytics events with the Seekora SDK.
12246
+ * Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
12247
+ * the SDK sends both legacy and v3 fields for backend compatibility.
10630
12248
  */
10631
12249
  const useAnalytics = ({ client, enabled = true, }) => {
10632
12250
  const trackEvent = useCallback(async (eventType, payload, context) => {
@@ -10646,14 +12264,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
10646
12264
  const trackClick = useCallback(async (resultId, result, context, position) => {
10647
12265
  if (!enabled)
10648
12266
  return;
10649
- await trackEvent('product_click', {
10650
- clicked_item_id: resultId,
10651
- metadata: {
10652
- result: result,
10653
- ...(position !== undefined && { position }),
10654
- },
10655
- }, context);
10656
- }, [trackEvent, enabled]);
12267
+ const pos = position ?? 0;
12268
+ if (client.trackClick) {
12269
+ await client.trackClick(resultId, pos, context);
12270
+ }
12271
+ else {
12272
+ await trackEvent('product_click', {
12273
+ clicked_item_id: resultId,
12274
+ metadata: { result, ...(position !== undefined && { position: pos }) },
12275
+ }, context);
12276
+ }
12277
+ }, [client, trackEvent, enabled]);
10657
12278
  const trackConversion = useCallback(async (resultId, result, value, currency, context) => {
10658
12279
  if (!enabled)
10659
12280
  return;
@@ -11675,5 +13296,5 @@ function updateSuggestionsStyles(theme) {
11675
13296
  injectSuggestionsStyles(theme, true);
11676
13297
  }
11677
13298
 
11678
- export { AmazonDropdown, Breadcrumb, ClearRefinements, CurrentRefinements, Facets, FederatedDropdown, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight, HitsPerPage, InfiniteHits, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchLayout, SearchProvider, SearchResults, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionSearchBar, TrendingItems, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getRecentSearches, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useInjectResponsiveStyles, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics };
13299
+ export { AmazonDropdown, Breadcrumb, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, InfiniteHits, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchLayout, SearchProvider, SearchResults, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionSearchBar, TrendingItems, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getFingerprint, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics };
11679
13300
  //# sourceMappingURL=index.esm.js.map