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

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 (58) hide show
  1. package/dist/components/SearchResults.d.ts.map +1 -1
  2. package/dist/components/SearchResults.js +24 -58
  3. package/dist/docsearch/components/DocSearch.d.ts +4 -0
  4. package/dist/docsearch/components/DocSearch.d.ts.map +1 -0
  5. package/dist/docsearch/components/DocSearch.js +81 -0
  6. package/dist/docsearch/components/DocSearchButton.d.ts +4 -0
  7. package/dist/docsearch/components/DocSearchButton.d.ts.map +1 -0
  8. package/dist/docsearch/components/DocSearchButton.js +12 -0
  9. package/dist/docsearch/components/Footer.d.ts +8 -0
  10. package/dist/docsearch/components/Footer.d.ts.map +1 -0
  11. package/dist/docsearch/components/Footer.js +23 -0
  12. package/dist/docsearch/components/Highlight.d.ts +9 -0
  13. package/dist/docsearch/components/Highlight.d.ts.map +1 -0
  14. package/dist/docsearch/components/Highlight.js +48 -0
  15. package/dist/docsearch/components/Hit.d.ts +15 -0
  16. package/dist/docsearch/components/Hit.d.ts.map +1 -0
  17. package/dist/docsearch/components/Hit.js +96 -0
  18. package/dist/docsearch/components/Modal.d.ts +9 -0
  19. package/dist/docsearch/components/Modal.d.ts.map +1 -0
  20. package/dist/docsearch/components/Modal.js +54 -0
  21. package/dist/docsearch/components/Results.d.ts +21 -0
  22. package/dist/docsearch/components/Results.d.ts.map +1 -0
  23. package/dist/docsearch/components/Results.js +91 -0
  24. package/dist/docsearch/components/SearchBox.d.ts +12 -0
  25. package/dist/docsearch/components/SearchBox.d.ts.map +1 -0
  26. package/dist/docsearch/components/SearchBox.js +18 -0
  27. package/dist/docsearch/hooks/useDocSearch.d.ts +32 -0
  28. package/dist/docsearch/hooks/useDocSearch.d.ts.map +1 -0
  29. package/dist/docsearch/hooks/useDocSearch.js +208 -0
  30. package/dist/docsearch/hooks/useKeyboard.d.ts +17 -0
  31. package/dist/docsearch/hooks/useKeyboard.d.ts.map +1 -0
  32. package/dist/docsearch/hooks/useKeyboard.js +71 -0
  33. package/dist/docsearch/hooks/useSeekoraSearch.d.ts +27 -0
  34. package/dist/docsearch/hooks/useSeekoraSearch.d.ts.map +1 -0
  35. package/dist/docsearch/hooks/useSeekoraSearch.js +187 -0
  36. package/dist/docsearch/index.d.ts +13 -0
  37. package/dist/docsearch/index.d.ts.map +1 -0
  38. package/dist/docsearch/index.js +11 -0
  39. package/dist/docsearch/types.d.ts +170 -0
  40. package/dist/docsearch/types.d.ts.map +1 -0
  41. package/dist/docsearch/types.js +4 -0
  42. package/dist/docsearch.css +237 -0
  43. package/dist/hooks/useAnalytics.d.ts +8 -4
  44. package/dist/hooks/useAnalytics.d.ts.map +1 -1
  45. package/dist/hooks/useAnalytics.js +14 -9
  46. package/dist/hooks/useSuggestionsAnalytics.d.ts +3 -1
  47. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  48. package/dist/hooks/useSuggestionsAnalytics.js +11 -9
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +11 -0
  52. package/dist/index.umd.js +1 -1
  53. package/dist/src/index.d.ts +258 -7
  54. package/dist/src/index.esm.js +1611 -79
  55. package/dist/src/index.esm.js.map +1 -1
  56. package/dist/src/index.js +1618 -78
  57. package/dist/src/index.js.map +1 -1
  58. package/package.json +8 -6
package/dist/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
+ var reactDom = require('react-dom');
5
+ var searchSdk = require('@seekora-ai/search-sdk');
4
6
 
5
7
  /**
6
8
  * Field Mapping Utilities
@@ -567,6 +569,688 @@ class SearchStateManager {
567
569
  }
568
570
  }
569
571
 
572
+ /**
573
+ * Seekora Device Fingerprinting Module
574
+ *
575
+ * A lightweight, dependency-free device fingerprinting solution that collects
576
+ * various browser and device signals to generate a unique visitor identifier.
577
+ */
578
+ // ============================================================================
579
+ // MurmurHash3 Implementation (32-bit)
580
+ // ============================================================================
581
+ function murmurHash3(str, seed = 0) {
582
+ const remainder = str.length & 3; // str.length % 4
583
+ const bytes = str.length - remainder;
584
+ let h1 = seed;
585
+ const c1 = 0xcc9e2d51;
586
+ const c2 = 0x1b873593;
587
+ let i = 0;
588
+ let k1;
589
+ while (i < bytes) {
590
+ k1 =
591
+ (str.charCodeAt(i) & 0xff) |
592
+ ((str.charCodeAt(++i) & 0xff) << 8) |
593
+ ((str.charCodeAt(++i) & 0xff) << 16) |
594
+ ((str.charCodeAt(++i) & 0xff) << 24);
595
+ ++i;
596
+ k1 = Math.imul(k1, c1);
597
+ k1 = (k1 << 15) | (k1 >>> 17);
598
+ k1 = Math.imul(k1, c2);
599
+ h1 ^= k1;
600
+ h1 = (h1 << 13) | (h1 >>> 19);
601
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
602
+ }
603
+ k1 = 0;
604
+ switch (remainder) {
605
+ case 3:
606
+ k1 ^= (str.charCodeAt(i + 2) & 0xff) << 16;
607
+ // fallthrough
608
+ case 2:
609
+ k1 ^= (str.charCodeAt(i + 1) & 0xff) << 8;
610
+ // fallthrough
611
+ case 1:
612
+ k1 ^= str.charCodeAt(i) & 0xff;
613
+ k1 = Math.imul(k1, c1);
614
+ k1 = (k1 << 15) | (k1 >>> 17);
615
+ k1 = Math.imul(k1, c2);
616
+ h1 ^= k1;
617
+ }
618
+ h1 ^= str.length;
619
+ h1 ^= h1 >>> 16;
620
+ h1 = Math.imul(h1, 0x85ebca6b);
621
+ h1 ^= h1 >>> 13;
622
+ h1 = Math.imul(h1, 0xc2b2ae35);
623
+ h1 ^= h1 >>> 16;
624
+ return h1 >>> 0;
625
+ }
626
+ function murmurHash3Hex(str, seed = 0) {
627
+ return murmurHash3(str, seed).toString(16).padStart(8, '0');
628
+ }
629
+ // ============================================================================
630
+ // Default Configuration
631
+ // ============================================================================
632
+ const DEFAULT_CONFIG = {
633
+ enableCanvas: true,
634
+ enableWebGL: true,
635
+ enableAudio: true,
636
+ enableFonts: true,
637
+ enableHardware: true,
638
+ timeout: 5000,
639
+ };
640
+ // ============================================================================
641
+ // Font List for Detection
642
+ // ============================================================================
643
+ const FONTS_TO_CHECK = [
644
+ // Windows fonts
645
+ 'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS',
646
+ 'Consolas', 'Courier New', 'Georgia', 'Impact', 'Lucida Console',
647
+ 'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype',
648
+ 'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',
649
+ // macOS fonts
650
+ 'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',
651
+ 'Apple SD Gothic Neo', 'Arial Hebrew', 'Avenir', 'Baskerville',
652
+ 'Big Caslon', 'Brush Script MT', 'Chalkboard', 'Cochin', 'Copperplate',
653
+ 'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica', 'Helvetica Neue',
654
+ 'Herculanum', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo',
655
+ 'Monaco', 'Noteworthy', 'Optima', 'Papyrus', 'Phosphate', 'Rockwell',
656
+ 'San Francisco', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand',
657
+ 'Zapfino',
658
+ // Linux fonts
659
+ 'Cantarell', 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif',
660
+ 'Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'FreeMono', 'FreeSans',
661
+ 'FreeSerif', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
662
+ 'Noto Sans', 'Noto Serif', 'Open Sans', 'Roboto', 'Ubuntu', 'Ubuntu Mono',
663
+ // Common web fonts
664
+ 'Lato', 'Montserrat', 'Oswald', 'Raleway', 'Source Sans Pro',
665
+ 'PT Sans', 'Merriweather', 'Nunito', 'Playfair Display', 'Poppins',
666
+ ];
667
+ // ============================================================================
668
+ // Fingerprint Class
669
+ // ============================================================================
670
+ class Fingerprint {
671
+ constructor(config) {
672
+ this.cachedResult = null;
673
+ this.config = { ...DEFAULT_CONFIG, ...config };
674
+ }
675
+ /**
676
+ * Get the complete fingerprint result
677
+ */
678
+ async get() {
679
+ if (this.cachedResult) {
680
+ return this.cachedResult;
681
+ }
682
+ const components = await this.collectComponents();
683
+ const confidence = this.calculateConfidence(components);
684
+ const visitorId = this.generateVisitorId(components);
685
+ this.cachedResult = {
686
+ visitorId,
687
+ confidence,
688
+ components,
689
+ };
690
+ return this.cachedResult;
691
+ }
692
+ /**
693
+ * Clear the cached result to force re-fingerprinting
694
+ */
695
+ clearCache() {
696
+ this.cachedResult = null;
697
+ }
698
+ /**
699
+ * Collect all fingerprint components
700
+ */
701
+ async collectComponents() {
702
+ const [canvas, webgl, audio, adBlockerDetected, incognitoDetected,] = await Promise.all([
703
+ this.config.enableCanvas ? this.getCanvasFingerprint() : { hash: '' },
704
+ this.config.enableWebGL ? this.getWebGLFingerprint() : { renderer: '', vendor: '', hash: '' },
705
+ this.config.enableAudio ? this.getAudioFingerprint() : { hash: '' },
706
+ this.detectAdBlocker(),
707
+ this.detectIncognito(),
708
+ ]);
709
+ const fonts = this.config.enableFonts ? this.detectFonts() : [];
710
+ const hardware = this.config.enableHardware ? this.getHardwareInfo() : {
711
+ concurrency: 0,
712
+ deviceMemory: 0,
713
+ maxTouchPoints: 0,
714
+ platform: '',
715
+ };
716
+ const screen = this.getScreenInfo();
717
+ const browser = this.getBrowserInfo();
718
+ return {
719
+ canvas,
720
+ webgl,
721
+ audio,
722
+ fonts,
723
+ hardware,
724
+ screen,
725
+ browser,
726
+ adBlockerDetected,
727
+ incognitoDetected,
728
+ };
729
+ }
730
+ /**
731
+ * Canvas fingerprinting - draws various shapes and text, then hashes the result
732
+ */
733
+ async getCanvasFingerprint() {
734
+ try {
735
+ const canvas = document.createElement('canvas');
736
+ canvas.width = 256;
737
+ canvas.height = 128;
738
+ const ctx = canvas.getContext('2d');
739
+ if (!ctx) {
740
+ return { hash: '' };
741
+ }
742
+ // Draw background
743
+ ctx.fillStyle = '#f8f8f8';
744
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
745
+ // Draw gradient
746
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
747
+ gradient.addColorStop(0, '#ff6b6b');
748
+ gradient.addColorStop(0.5, '#4ecdc4');
749
+ gradient.addColorStop(1, '#45b7d1');
750
+ ctx.fillStyle = gradient;
751
+ ctx.fillRect(10, 10, 100, 50);
752
+ // Draw text with different fonts
753
+ ctx.font = '18px Arial';
754
+ ctx.fillStyle = '#333';
755
+ ctx.textBaseline = 'alphabetic';
756
+ ctx.fillText('Seekora Fingerprint 🔐', 20, 90);
757
+ // Draw more text with different styling
758
+ ctx.font = 'bold 14px Georgia';
759
+ ctx.fillStyle = 'rgba(102, 204, 153, 0.7)';
760
+ ctx.fillText('Test String @#$%', 130, 40);
761
+ // Draw shapes
762
+ ctx.beginPath();
763
+ ctx.arc(200, 70, 30, 0, Math.PI * 2);
764
+ ctx.fillStyle = 'rgba(255, 165, 0, 0.5)';
765
+ ctx.fill();
766
+ ctx.strokeStyle = '#333';
767
+ ctx.lineWidth = 2;
768
+ ctx.stroke();
769
+ // Draw bezier curve
770
+ ctx.beginPath();
771
+ ctx.moveTo(10, 120);
772
+ ctx.bezierCurveTo(50, 80, 100, 120, 140, 100);
773
+ ctx.strokeStyle = '#9b59b6';
774
+ ctx.lineWidth = 3;
775
+ ctx.stroke();
776
+ // Draw rotated rectangle
777
+ ctx.save();
778
+ ctx.translate(180, 100);
779
+ ctx.rotate(0.5);
780
+ ctx.fillStyle = 'rgba(52, 152, 219, 0.6)';
781
+ ctx.fillRect(-20, -10, 40, 20);
782
+ ctx.restore();
783
+ const data = canvas.toDataURL('image/png');
784
+ const hash = murmurHash3Hex(data);
785
+ return { hash, data };
786
+ }
787
+ catch {
788
+ return { hash: '' };
789
+ }
790
+ }
791
+ /**
792
+ * WebGL fingerprinting - extracts GPU info and renders test triangles
793
+ */
794
+ async getWebGLFingerprint() {
795
+ try {
796
+ const canvas = document.createElement('canvas');
797
+ canvas.width = 256;
798
+ canvas.height = 256;
799
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
800
+ if (!gl) {
801
+ return { renderer: '', vendor: '', hash: '' };
802
+ }
803
+ // Get debug info extension for renderer/vendor
804
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
805
+ const renderer = debugInfo
806
+ ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || ''
807
+ : '';
808
+ const vendor = debugInfo
809
+ ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || ''
810
+ : '';
811
+ // Collect WebGL parameters
812
+ const params = [];
813
+ // Basic parameters
814
+ const paramIds = [
815
+ gl.ALIASED_LINE_WIDTH_RANGE,
816
+ gl.ALIASED_POINT_SIZE_RANGE,
817
+ gl.ALPHA_BITS,
818
+ gl.BLUE_BITS,
819
+ gl.DEPTH_BITS,
820
+ gl.GREEN_BITS,
821
+ gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS,
822
+ gl.MAX_CUBE_MAP_TEXTURE_SIZE,
823
+ gl.MAX_FRAGMENT_UNIFORM_VECTORS,
824
+ gl.MAX_RENDERBUFFER_SIZE,
825
+ gl.MAX_TEXTURE_IMAGE_UNITS,
826
+ gl.MAX_TEXTURE_SIZE,
827
+ gl.MAX_VARYING_VECTORS,
828
+ gl.MAX_VERTEX_ATTRIBS,
829
+ gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS,
830
+ gl.MAX_VERTEX_UNIFORM_VECTORS,
831
+ gl.MAX_VIEWPORT_DIMS,
832
+ gl.RED_BITS,
833
+ gl.RENDERER,
834
+ gl.SHADING_LANGUAGE_VERSION,
835
+ gl.STENCIL_BITS,
836
+ gl.VENDOR,
837
+ gl.VERSION,
838
+ ];
839
+ for (const paramId of paramIds) {
840
+ try {
841
+ const value = gl.getParameter(paramId);
842
+ if (value !== null && value !== undefined) {
843
+ if (value instanceof Float32Array || value instanceof Int32Array) {
844
+ params.push(Array.from(value).join(','));
845
+ }
846
+ else {
847
+ params.push(String(value));
848
+ }
849
+ }
850
+ }
851
+ catch {
852
+ // Parameter not available
853
+ }
854
+ }
855
+ // Get supported extensions
856
+ const extensions = gl.getSupportedExtensions() || [];
857
+ params.push(extensions.sort().join(','));
858
+ // Render a test triangle and get pixel data
859
+ try {
860
+ const vertexShaderSource = `
861
+ attribute vec2 position;
862
+ void main() {
863
+ gl_Position = vec4(position, 0.0, 1.0);
864
+ }
865
+ `;
866
+ const fragmentShaderSource = `
867
+ precision mediump float;
868
+ void main() {
869
+ gl_FragColor = vec4(0.812, 0.373, 0.784, 1.0);
870
+ }
871
+ `;
872
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
873
+ gl.shaderSource(vertexShader, vertexShaderSource);
874
+ gl.compileShader(vertexShader);
875
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
876
+ gl.shaderSource(fragmentShader, fragmentShaderSource);
877
+ gl.compileShader(fragmentShader);
878
+ const program = gl.createProgram();
879
+ gl.attachShader(program, vertexShader);
880
+ gl.attachShader(program, fragmentShader);
881
+ gl.linkProgram(program);
882
+ gl.useProgram(program);
883
+ const vertices = new Float32Array([
884
+ 0.0, 0.5,
885
+ -0.5, -0.5,
886
+ 0.5, -0.5,
887
+ ]);
888
+ const buffer = gl.createBuffer();
889
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
890
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
891
+ const positionLocation = gl.getAttribLocation(program, 'position');
892
+ gl.enableVertexAttribArray(positionLocation);
893
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
894
+ gl.clearColor(0.1, 0.1, 0.1, 1.0);
895
+ gl.clear(gl.COLOR_BUFFER_BIT);
896
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
897
+ const pixels = new Uint8Array(256 * 256 * 4);
898
+ gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
899
+ // Sample some pixels for the hash
900
+ const sampleIndices = [0, 1000, 5000, 10000, 25000, 50000, 100000, 200000];
901
+ for (const idx of sampleIndices) {
902
+ if (idx < pixels.length) {
903
+ params.push(String(pixels[idx]));
904
+ }
905
+ }
906
+ }
907
+ catch {
908
+ // Shader compilation/rendering failed
909
+ }
910
+ const dataString = `${renderer}|${vendor}|${params.join('|')}`;
911
+ const hash = murmurHash3Hex(dataString);
912
+ return { renderer, vendor, hash };
913
+ }
914
+ catch {
915
+ return { renderer: '', vendor: '', hash: '' };
916
+ }
917
+ }
918
+ /**
919
+ * Audio fingerprinting using AudioContext oscillator
920
+ */
921
+ async getAudioFingerprint() {
922
+ try {
923
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
924
+ if (!AudioContext) {
925
+ return { hash: '' };
926
+ }
927
+ const context = new AudioContext();
928
+ // Create oscillator
929
+ const oscillator = context.createOscillator();
930
+ oscillator.type = 'triangle';
931
+ oscillator.frequency.setValueAtTime(10000, context.currentTime);
932
+ // Create compressor
933
+ const compressor = context.createDynamicsCompressor();
934
+ compressor.threshold.setValueAtTime(-50, context.currentTime);
935
+ compressor.knee.setValueAtTime(40, context.currentTime);
936
+ compressor.ratio.setValueAtTime(12, context.currentTime);
937
+ compressor.attack.setValueAtTime(0, context.currentTime);
938
+ compressor.release.setValueAtTime(0.25, context.currentTime);
939
+ // Create analyser
940
+ const analyser = context.createAnalyser();
941
+ analyser.fftSize = 2048;
942
+ // Connect nodes
943
+ oscillator.connect(compressor);
944
+ compressor.connect(analyser);
945
+ analyser.connect(context.destination);
946
+ // Start and get data
947
+ oscillator.start(0);
948
+ return new Promise((resolve) => {
949
+ const timeout = setTimeout(() => {
950
+ try {
951
+ oscillator.stop();
952
+ context.close();
953
+ }
954
+ catch {
955
+ // Ignore cleanup errors
956
+ }
957
+ resolve({ hash: '' });
958
+ }, this.config.timeout);
959
+ // Give it a moment to process
960
+ setTimeout(() => {
961
+ try {
962
+ const dataArray = new Float32Array(analyser.frequencyBinCount);
963
+ analyser.getFloatFrequencyData(dataArray);
964
+ // Calculate a fingerprint value from the audio data
965
+ let sum = 0;
966
+ let count = 0;
967
+ for (let i = 0; i < dataArray.length; i++) {
968
+ if (isFinite(dataArray[i]) && dataArray[i] !== 0) {
969
+ sum += Math.abs(dataArray[i]);
970
+ count++;
971
+ }
972
+ }
973
+ const value = count > 0 ? sum / count : 0;
974
+ const dataString = Array.from(dataArray.slice(0, 100))
975
+ .filter(v => isFinite(v))
976
+ .map(v => v.toFixed(4))
977
+ .join(',');
978
+ clearTimeout(timeout);
979
+ oscillator.stop();
980
+ context.close();
981
+ const hash = murmurHash3Hex(dataString + String(value));
982
+ resolve({ hash, value });
983
+ }
984
+ catch {
985
+ clearTimeout(timeout);
986
+ try {
987
+ oscillator.stop();
988
+ context.close();
989
+ }
990
+ catch {
991
+ // Ignore cleanup errors
992
+ }
993
+ resolve({ hash: '' });
994
+ }
995
+ }, 200);
996
+ });
997
+ }
998
+ catch {
999
+ return { hash: '' };
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Font detection by measuring text rendering differences
1004
+ */
1005
+ detectFonts() {
1006
+ const detectedFonts = [];
1007
+ try {
1008
+ const baseFonts = ['monospace', 'sans-serif', 'serif'];
1009
+ const testString = 'mmmmmmmmmmlli';
1010
+ const testSize = '72px';
1011
+ const canvas = document.createElement('canvas');
1012
+ canvas.width = 500;
1013
+ canvas.height = 100;
1014
+ const ctx = canvas.getContext('2d');
1015
+ if (!ctx) {
1016
+ return detectedFonts;
1017
+ }
1018
+ // Get base widths
1019
+ const baseWidths = {};
1020
+ for (const baseFont of baseFonts) {
1021
+ ctx.font = `${testSize} ${baseFont}`;
1022
+ baseWidths[baseFont] = ctx.measureText(testString).width;
1023
+ }
1024
+ // Check each font
1025
+ for (const font of FONTS_TO_CHECK) {
1026
+ let detected = false;
1027
+ for (const baseFont of baseFonts) {
1028
+ ctx.font = `${testSize} "${font}", ${baseFont}`;
1029
+ const width = ctx.measureText(testString).width;
1030
+ if (width !== baseWidths[baseFont]) {
1031
+ detected = true;
1032
+ break;
1033
+ }
1034
+ }
1035
+ if (detected) {
1036
+ detectedFonts.push(font);
1037
+ }
1038
+ }
1039
+ }
1040
+ catch {
1041
+ // Font detection failed
1042
+ }
1043
+ return detectedFonts;
1044
+ }
1045
+ /**
1046
+ * Get hardware information
1047
+ */
1048
+ getHardwareInfo() {
1049
+ const nav = navigator;
1050
+ return {
1051
+ concurrency: nav.hardwareConcurrency || 0,
1052
+ deviceMemory: nav.deviceMemory || 0,
1053
+ maxTouchPoints: nav.maxTouchPoints || 0,
1054
+ platform: nav.platform || '',
1055
+ };
1056
+ }
1057
+ /**
1058
+ * Get screen information
1059
+ */
1060
+ getScreenInfo() {
1061
+ return {
1062
+ width: screen.width || 0,
1063
+ height: screen.height || 0,
1064
+ colorDepth: screen.colorDepth || 0,
1065
+ pixelRatio: window.devicePixelRatio || 1,
1066
+ };
1067
+ }
1068
+ /**
1069
+ * Get browser information
1070
+ */
1071
+ getBrowserInfo() {
1072
+ const nav = navigator;
1073
+ return {
1074
+ language: nav.language || '',
1075
+ languages: Array.from(nav.languages || []),
1076
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
1077
+ timezoneOffset: new Date().getTimezoneOffset(),
1078
+ cookieEnabled: nav.cookieEnabled ?? false,
1079
+ doNotTrack: nav.doNotTrack === '1' || nav.doNotTrack === 'yes',
1080
+ };
1081
+ }
1082
+ /**
1083
+ * Detect if an ad blocker is installed
1084
+ */
1085
+ async detectAdBlocker() {
1086
+ try {
1087
+ // Create a bait element
1088
+ const bait = document.createElement('div');
1089
+ bait.className = 'adsbox ad-banner ad_banner textAd text_ad text-ad';
1090
+ bait.style.cssText = 'position: absolute; left: -9999px; width: 1px; height: 1px;';
1091
+ bait.innerHTML = '&nbsp;';
1092
+ document.body.appendChild(bait);
1093
+ // Wait a bit for ad blockers to act
1094
+ await new Promise(resolve => setTimeout(resolve, 100));
1095
+ const isBlocked = bait.offsetParent === null ||
1096
+ bait.offsetHeight === 0 ||
1097
+ bait.offsetWidth === 0 ||
1098
+ getComputedStyle(bait).display === 'none' ||
1099
+ getComputedStyle(bait).visibility === 'hidden';
1100
+ document.body.removeChild(bait);
1101
+ // Also try to fetch a known ad-related resource
1102
+ try {
1103
+ const response = await fetch('https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', {
1104
+ method: 'HEAD',
1105
+ mode: 'no-cors',
1106
+ });
1107
+ // If we get here without error, no ad blocker (though no-cors doesn't give us much info)
1108
+ return isBlocked;
1109
+ }
1110
+ catch {
1111
+ return true; // Blocked
1112
+ }
1113
+ }
1114
+ catch {
1115
+ return false;
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Detect if browser is in incognito/private mode
1120
+ */
1121
+ async detectIncognito() {
1122
+ // Chrome/Chromium detection
1123
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
1124
+ try {
1125
+ const estimate = await navigator.storage.estimate();
1126
+ // In incognito, quota is typically limited to ~120MB
1127
+ if (estimate.quota && estimate.quota < 130 * 1024 * 1024) {
1128
+ return true;
1129
+ }
1130
+ }
1131
+ catch {
1132
+ // Storage API not available
1133
+ }
1134
+ }
1135
+ // Firefox detection using IndexedDB
1136
+ try {
1137
+ const db = indexedDB.open('test-private-mode');
1138
+ return new Promise((resolve) => {
1139
+ db.onerror = () => resolve(true);
1140
+ db.onsuccess = () => {
1141
+ db.result.close();
1142
+ indexedDB.deleteDatabase('test-private-mode');
1143
+ resolve(false);
1144
+ };
1145
+ // Timeout fallback
1146
+ setTimeout(() => resolve(false), 500);
1147
+ });
1148
+ }
1149
+ catch {
1150
+ // IndexedDB not available - might be incognito
1151
+ return true;
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Generate a hash from any data
1156
+ */
1157
+ generateHash(data) {
1158
+ return murmurHash3Hex(data);
1159
+ }
1160
+ /**
1161
+ * Generate the final visitor ID from all components
1162
+ */
1163
+ generateVisitorId(components) {
1164
+ const data = JSON.stringify({
1165
+ canvas: components.canvas.hash,
1166
+ webgl: components.webgl.hash,
1167
+ audio: components.audio.hash,
1168
+ fonts: components.fonts.sort().join(','),
1169
+ hardware: components.hardware,
1170
+ screen: components.screen,
1171
+ browser: {
1172
+ ...components.browser,
1173
+ languages: components.browser.languages.sort().join(','),
1174
+ },
1175
+ });
1176
+ // Generate a 32-character hex string (similar to UUID format)
1177
+ const hash1 = murmurHash3Hex(data, 0);
1178
+ const hash2 = murmurHash3Hex(data, 1);
1179
+ const hash3 = murmurHash3Hex(data, 2);
1180
+ const hash4 = murmurHash3Hex(data, 3);
1181
+ return `${hash1}${hash2}${hash3}${hash4}`;
1182
+ }
1183
+ /**
1184
+ * Calculate confidence score based on available components
1185
+ */
1186
+ calculateConfidence(components) {
1187
+ let score = 0;
1188
+ let maxScore = 0;
1189
+ // Canvas (high entropy)
1190
+ maxScore += 0.2;
1191
+ if (components.canvas.hash) {
1192
+ score += 0.2;
1193
+ }
1194
+ // WebGL (high entropy)
1195
+ maxScore += 0.2;
1196
+ if (components.webgl.hash && components.webgl.renderer) {
1197
+ score += 0.2;
1198
+ }
1199
+ else if (components.webgl.hash) {
1200
+ score += 0.1;
1201
+ }
1202
+ // Audio (medium entropy)
1203
+ maxScore += 0.15;
1204
+ if (components.audio.hash) {
1205
+ score += 0.15;
1206
+ }
1207
+ // Fonts (high entropy)
1208
+ maxScore += 0.15;
1209
+ if (components.fonts.length > 10) {
1210
+ score += 0.15;
1211
+ }
1212
+ else if (components.fonts.length > 5) {
1213
+ score += 0.1;
1214
+ }
1215
+ else if (components.fonts.length > 0) {
1216
+ score += 0.05;
1217
+ }
1218
+ // Hardware (medium entropy)
1219
+ maxScore += 0.1;
1220
+ if (components.hardware.concurrency > 0 && components.hardware.platform) {
1221
+ score += 0.1;
1222
+ }
1223
+ else if (components.hardware.platform) {
1224
+ score += 0.05;
1225
+ }
1226
+ // Screen (low entropy but stable)
1227
+ maxScore += 0.1;
1228
+ if (components.screen.width > 0 && components.screen.height > 0) {
1229
+ score += 0.1;
1230
+ }
1231
+ // Browser (low entropy but stable)
1232
+ maxScore += 0.1;
1233
+ if (components.browser.timezone && components.browser.language) {
1234
+ score += 0.1;
1235
+ }
1236
+ else if (components.browser.language) {
1237
+ score += 0.05;
1238
+ }
1239
+ // Normalize and return
1240
+ return Math.min(1, score / maxScore);
1241
+ }
1242
+ }
1243
+ // ============================================================================
1244
+ // Convenience Function
1245
+ // ============================================================================
1246
+ /**
1247
+ * Quick fingerprint getter - creates a Fingerprint instance and returns the result
1248
+ */
1249
+ async function getFingerprint(config) {
1250
+ const fingerprint = new Fingerprint(config);
1251
+ return fingerprint.get();
1252
+ }
1253
+
570
1254
  /**
571
1255
  * Default Theme
572
1256
  */
@@ -1135,38 +1819,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
1135
1819
  const state = { currentPage, itemsPerPage: stateItemsPerPage };
1136
1820
  // Calculate absolute position (1-based) accounting for pagination
1137
1821
  const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
1138
- // Build search context from current state and response
1139
- const searchContext = results?.context || (results ? {
1140
- query: '', // Will be filled from state if needed
1141
- page: state.currentPage,
1142
- } : undefined);
1143
- console.log('🔵 SearchResults: Tracking analytics event', {
1144
- resultId: result.id,
1145
- resultIndex: index,
1146
- absolutePosition,
1147
- currentPage: state.currentPage,
1148
- itemsPerPage: state.itemsPerPage,
1149
- hasContext: !!searchContext,
1150
- });
1151
- await client.trackEvent?.({
1152
- event_name: 'product_click',
1153
- clicked_item_id: result.id,
1154
- metadata: {
1155
- result: result,
1156
- position: absolutePosition,
1157
- },
1158
- }, searchContext);
1159
- console.log('🟢 SearchResults: Analytics event tracked successfully', {
1160
- resultId: result.id,
1161
- position: absolutePosition,
1162
- });
1822
+ const searchContext = results?.context;
1823
+ if (client.trackClick) {
1824
+ await client.trackClick(result.id, absolutePosition, searchContext);
1825
+ }
1826
+ else {
1827
+ await client.trackEvent?.({
1828
+ event_name: 'product_click',
1829
+ clicked_item_id: result.id,
1830
+ metadata: { result, position: absolutePosition },
1831
+ }, searchContext);
1832
+ }
1163
1833
  }
1164
1834
  catch (err) {
1165
1835
  const error = err instanceof Error ? err : new Error(String(err));
1166
- console.error('SearchResults: Error tracking analytics event', {
1167
- resultId: result.id,
1168
- error: error.message,
1169
- });
1836
+ log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
1170
1837
  }
1171
1838
  }
1172
1839
  // Call user-provided callback
@@ -1226,38 +1893,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
1226
1893
  const state = { currentPage, itemsPerPage: stateItemsPerPage };
1227
1894
  // Calculate absolute position (1-based) accounting for pagination
1228
1895
  const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
1229
- // Build search context from current state and response
1230
- const searchContext = results?.context || (results ? {
1231
- query: '', // Will be filled from state if needed
1232
- page: state.currentPage,
1233
- } : undefined);
1234
- console.log('🔵 SearchResults: Tracking analytics event', {
1235
- resultId: result.id,
1236
- resultIndex: index,
1237
- absolutePosition,
1238
- currentPage: state.currentPage,
1239
- itemsPerPage: state.itemsPerPage,
1240
- hasContext: !!searchContext,
1241
- });
1242
- await client.trackEvent?.({
1243
- event_name: 'product_click',
1244
- clicked_item_id: result.id,
1245
- metadata: {
1246
- result: result,
1247
- position: absolutePosition,
1248
- },
1249
- }, searchContext);
1250
- console.log('🟢 SearchResults: Analytics event tracked successfully', {
1251
- resultId: result.id,
1252
- position: absolutePosition,
1253
- });
1896
+ const searchContext = results?.context;
1897
+ if (client.trackClick) {
1898
+ await client.trackClick(result.id, absolutePosition, searchContext);
1899
+ }
1900
+ else {
1901
+ await client.trackEvent?.({
1902
+ event_name: 'product_click',
1903
+ clicked_item_id: result.id,
1904
+ metadata: { result, position: absolutePosition },
1905
+ }, searchContext);
1906
+ }
1254
1907
  }
1255
1908
  catch (err) {
1256
1909
  const error = err instanceof Error ? err : new Error(String(err));
1257
- console.error('SearchResults: Error tracking analytics event', {
1258
- resultId: result.id,
1259
- error: error.message,
1260
- });
1910
+ log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
1261
1911
  }
1262
1912
  }
1263
1913
  // Call user-provided callback
@@ -2615,7 +3265,7 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
2615
3265
  *
2616
3266
  * Renders highlighted search result text with matching terms emphasized
2617
3267
  */
2618
- const Highlight = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
3268
+ const Highlight$1 = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
2619
3269
  const { theme } = useSearchContext();
2620
3270
  const highlightTheme = customTheme || {};
2621
3271
  // Get highlighted value from hit
@@ -5589,7 +6239,7 @@ const EVENTS = {
5589
6239
  // Hook Implementation
5590
6240
  // ============================================================================
5591
6241
  function useSuggestionsAnalytics(options) {
5592
- 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;
5593
6243
  // Refs for debouncing and tracking
5594
6244
  const impressionTimerRef = React.useRef(null);
5595
6245
  const lastImpressionRef = React.useRef(null);
@@ -5602,10 +6252,11 @@ function useSuggestionsAnalytics(options) {
5602
6252
  }
5603
6253
  };
5604
6254
  }, []);
5605
- // Helper to send event
5606
- const sendEvent = React.useCallback(async (eventName, metadata) => {
6255
+ // Helper to send event (optional context links event to search for v3 analytics)
6256
+ const sendEvent = React.useCallback(async (eventName, metadata, context) => {
5607
6257
  if (!enabled || !client)
5608
6258
  return;
6259
+ const searchContext = context ?? contextOption;
5609
6260
  try {
5610
6261
  await client.trackEvent?.({
5611
6262
  event_name: eventName,
@@ -5615,13 +6266,13 @@ function useSuggestionsAnalytics(options) {
5615
6266
  timestamp: Date.now(),
5616
6267
  source: 'suggestions_dropdown',
5617
6268
  },
5618
- });
6269
+ }, searchContext);
5619
6270
  log.verbose(`Analytics: ${eventName}`, metadata);
5620
6271
  }
5621
6272
  catch (error) {
5622
6273
  log.warn(`Failed to track ${eventName}`, { error });
5623
6274
  }
5624
- }, [client, enabled, analyticsTags]);
6275
+ }, [client, enabled, analyticsTags, contextOption]);
5625
6276
  // Track suggestion click
5626
6277
  const trackSuggestionClick = React.useCallback((data) => {
5627
6278
  if (!trackClicks)
@@ -5649,11 +6300,12 @@ function useSuggestionsAnalytics(options) {
5649
6300
  tab_id: data.tabId,
5650
6301
  original_query: data.query,
5651
6302
  });
5652
- // Also track as a general product click for analytics
5653
- if (client) {
5654
- 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(() => { });
5655
6307
  }
5656
- }, [client, sendEvent, trackClicks]);
6308
+ }, [client, contextOption, sendEvent, trackClicks]);
5657
6309
  // Track category click
5658
6310
  const trackCategoryClick = React.useCallback((category, query) => {
5659
6311
  if (!trackClicks)
@@ -10563,6 +11215,881 @@ const SuggestionDropdownVariants = {
10563
11215
  minimal: MinimalDropdown,
10564
11216
  };
10565
11217
 
11218
+ function Modal({ isOpen, onClose, children }) {
11219
+ const overlayRef = React.useRef(null);
11220
+ const containerRef = React.useRef(null);
11221
+ React.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
+ React.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
+ React.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 reactDom.createPortal(modalContent, document.body);
11269
+ }
11270
+
11271
+ function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
11272
+ const inputRef = React.useRef(null);
11273
+ React.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
+ function groupHitsByHierarchy(hits) {
11442
+ const groups = new Map();
11443
+ for (const hit of hits) {
11444
+ const lvl0 = hit.hierarchy?.lvl0 || '';
11445
+ if (!groups.has(lvl0))
11446
+ groups.set(lvl0, []);
11447
+ groups.get(lvl0).push(hit);
11448
+ }
11449
+ const result = [];
11450
+ for (const [lvl0Key, groupHits] of groups.entries()) {
11451
+ const sortedHits = [...groupHits].sort((a, b) => {
11452
+ const aType = a.type;
11453
+ const bType = b.type;
11454
+ return getTypeLevel(aType) - getTypeLevel(bType);
11455
+ });
11456
+ const markedHits = sortedHits.map((hit, index) => {
11457
+ const suggestion = hit;
11458
+ const isChild = isChildType(suggestion.type);
11459
+ const nextHit = sortedHits[index + 1];
11460
+ const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
11461
+ return { ...hit, isChild, isLastChild };
11462
+ });
11463
+ result.push({ category: lvl0Key || null, hits: markedHits });
11464
+ }
11465
+ return result;
11466
+ }
11467
+ function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
11468
+ let index = 0;
11469
+ for (let i = 0; i < groupIndex; i++)
11470
+ index += groups[i].hits.length;
11471
+ return index + hitIndex;
11472
+ }
11473
+ function getHitKey(hit, index) {
11474
+ if ('objectID' in hit)
11475
+ return hit.objectID;
11476
+ return `suggestion-${hit.url}-${index}`;
11477
+ }
11478
+ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, query, isLoading, error, translations = {}, sources: _sources = [], }) {
11479
+ const listRef = React.useRef(null);
11480
+ React.useEffect(() => {
11481
+ if (listRef.current && hits.length > 0) {
11482
+ const selectedItem = listRef.current.children[selectedIndex];
11483
+ if (selectedItem)
11484
+ selectedItem.scrollIntoView({ block: 'nearest' });
11485
+ }
11486
+ }, [selectedIndex, hits.length]);
11487
+ if (!query) {
11488
+ return (React.createElement("div", { className: "seekora-docsearch-empty" },
11489
+ React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
11490
+ }
11491
+ if (isLoading && hits.length === 0) {
11492
+ return (React.createElement("div", { className: "seekora-docsearch-loading" },
11493
+ React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
11494
+ React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
11495
+ React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
11496
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
11497
+ React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
11498
+ }
11499
+ if (error) {
11500
+ return (React.createElement("div", { className: "seekora-docsearch-error" },
11501
+ React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
11502
+ }
11503
+ if (hits.length === 0 && query) {
11504
+ return (React.createElement("div", { className: "seekora-docsearch-no-results" },
11505
+ React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
11506
+ }
11507
+ const displayGroups = groupedHits && groupedHits.length > 0
11508
+ ? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
11509
+ : groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
11510
+ return (React.createElement("div", { className: "seekora-docsearch-results" },
11511
+ 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" },
11512
+ group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
11513
+ React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
11514
+ const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
11515
+ const extHit = hit;
11516
+ return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
11517
+ 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 })));
11518
+ }))))))));
11519
+ }
11520
+
11521
+ function Footer({ translations = {} }) {
11522
+ return (React.createElement("footer", { className: "seekora-docsearch-footer" },
11523
+ React.createElement("div", { className: "seekora-docsearch-footer-commands" },
11524
+ React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
11525
+ React.createElement("li", null,
11526
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11527
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
11528
+ React.createElement("span", null, "to select"))),
11529
+ React.createElement("li", null,
11530
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11531
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
11532
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
11533
+ React.createElement("span", null, "to navigate"))),
11534
+ React.createElement("li", null,
11535
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11536
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
11537
+ React.createElement("span", null, translations.closeText || 'to close'))))),
11538
+ React.createElement("div", { className: "seekora-docsearch-footer-logo" },
11539
+ React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
11540
+ React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
11541
+ React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
11542
+ }
11543
+
11544
+ function useKeyboard(options) {
11545
+ const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
11546
+ const handleGlobalKeyDown = React.useCallback((event) => {
11547
+ if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
11548
+ event.preventDefault();
11549
+ if (isOpen) {
11550
+ onClose();
11551
+ }
11552
+ else {
11553
+ onOpen();
11554
+ }
11555
+ return;
11556
+ }
11557
+ if (!disableShortcut && event.key === '/' && !isOpen) {
11558
+ const target = event.target;
11559
+ const isInput = target.tagName === 'INPUT' ||
11560
+ target.tagName === 'TEXTAREA' ||
11561
+ target.isContentEditable;
11562
+ if (!isInput) {
11563
+ event.preventDefault();
11564
+ onOpen();
11565
+ }
11566
+ }
11567
+ }, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
11568
+ const handleModalKeyDown = React.useCallback((event) => {
11569
+ switch (event.key) {
11570
+ case 'Escape':
11571
+ event.preventDefault();
11572
+ onClose();
11573
+ break;
11574
+ case 'ArrowDown':
11575
+ event.preventDefault();
11576
+ onSelectNext();
11577
+ break;
11578
+ case 'ArrowUp':
11579
+ event.preventDefault();
11580
+ onSelectPrev();
11581
+ break;
11582
+ case 'Enter':
11583
+ event.preventDefault();
11584
+ onEnter();
11585
+ break;
11586
+ case 'Tab':
11587
+ if (event.shiftKey) {
11588
+ onSelectPrev();
11589
+ }
11590
+ else {
11591
+ onSelectNext();
11592
+ }
11593
+ event.preventDefault();
11594
+ break;
11595
+ }
11596
+ }, [onClose, onSelectNext, onSelectPrev, onEnter]);
11597
+ React.useEffect(() => {
11598
+ document.addEventListener('keydown', handleGlobalKeyDown);
11599
+ return () => {
11600
+ document.removeEventListener('keydown', handleGlobalKeyDown);
11601
+ };
11602
+ }, [handleGlobalKeyDown]);
11603
+ return {
11604
+ handleModalKeyDown,
11605
+ };
11606
+ }
11607
+ function getShortcutText(key = 'K') {
11608
+ if (typeof navigator === 'undefined') {
11609
+ return `⌘${key}`;
11610
+ }
11611
+ const isMac = navigator.platform.toLowerCase().includes('mac');
11612
+ return isMac ? `⌘${key}` : `Ctrl+${key}`;
11613
+ }
11614
+
11615
+ function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
11616
+ const shortcutText = getShortcutText('K');
11617
+ return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
11618
+ React.createElement("span", { className: "seekora-docsearch-button-icon" },
11619
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11620
+ 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" }))),
11621
+ React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
11622
+ React.createElement("span", { className: "seekora-docsearch-button-keys" },
11623
+ React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
11624
+ }
11625
+
11626
+ const initialState = {
11627
+ query: '',
11628
+ results: [],
11629
+ suggestions: [],
11630
+ groupedSuggestions: [],
11631
+ isLoading: false,
11632
+ error: null,
11633
+ selectedIndex: 0,
11634
+ mode: 'suggestions',
11635
+ };
11636
+ function reducer(state, action) {
11637
+ switch (action.type) {
11638
+ case 'SET_QUERY':
11639
+ return { ...state, query: action.payload, selectedIndex: 0 };
11640
+ case 'SET_RESULTS':
11641
+ return { ...state, results: action.payload, mode: 'results' };
11642
+ case 'SET_SUGGESTIONS':
11643
+ return { ...state, suggestions: action.payload, mode: 'suggestions' };
11644
+ case 'SET_GROUPED_SUGGESTIONS': {
11645
+ const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
11646
+ return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
11647
+ }
11648
+ case 'SET_LOADING':
11649
+ return { ...state, isLoading: action.payload };
11650
+ case 'SET_ERROR':
11651
+ return { ...state, error: action.payload };
11652
+ case 'SET_SELECTED_INDEX':
11653
+ return { ...state, selectedIndex: action.payload };
11654
+ case 'SELECT_NEXT': {
11655
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11656
+ const maxIndex = items.length - 1;
11657
+ return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
11658
+ }
11659
+ case 'SELECT_PREV': {
11660
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11661
+ const maxIndex = items.length - 1;
11662
+ return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
11663
+ }
11664
+ case 'SET_MODE':
11665
+ return { ...state, mode: action.payload, selectedIndex: 0 };
11666
+ case 'RESET':
11667
+ return initialState;
11668
+ default:
11669
+ return state;
11670
+ }
11671
+ }
11672
+ function useDocSearch(options) {
11673
+ const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200 } = options;
11674
+ const searchSources = sources || (apiEndpoint ? [{
11675
+ id: 'default',
11676
+ name: 'Results',
11677
+ endpoint: apiEndpoint,
11678
+ apiKey,
11679
+ maxResults,
11680
+ }] : []);
11681
+ const [state, dispatch] = React.useReducer(reducer, initialState);
11682
+ const abortControllersRef = React.useRef(new Map());
11683
+ const debounceTimerRef = React.useRef(null);
11684
+ const defaultTransform = (data, sourceId) => {
11685
+ const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
11686
+ return items.map((item) => ({
11687
+ url: item.url || item.route || '',
11688
+ title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
11689
+ content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
11690
+ description: item.description || item.content?.substring?.(0, 100) || '',
11691
+ category: item.category || item.hierarchy?.lvl0 || '',
11692
+ hierarchy: item.hierarchy,
11693
+ route: item.route,
11694
+ parentTitle: item.parent_title || item.parentTitle,
11695
+ _source: sourceId,
11696
+ }));
11697
+ };
11698
+ const transformPublicSearchResults = React.useCallback((data, sourceId) => {
11699
+ const results = data?.data?.results || [];
11700
+ return results.map((item) => {
11701
+ const doc = item.document || item;
11702
+ return {
11703
+ url: doc.url || doc.route || '',
11704
+ title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
11705
+ content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
11706
+ description: doc.description || doc.content?.substring?.(0, 100) || '',
11707
+ category: doc.category || doc.hierarchy?.lvl0 || '',
11708
+ hierarchy: doc.hierarchy,
11709
+ route: doc.route,
11710
+ parentTitle: doc.parent_title || doc.parentTitle,
11711
+ _source: sourceId,
11712
+ };
11713
+ });
11714
+ }, []);
11715
+ const fetchFromSource = React.useCallback(async (source, query, signal) => {
11716
+ const minLength = source.minQueryLength ?? 1;
11717
+ if (query.length < minLength)
11718
+ return [];
11719
+ try {
11720
+ if (source.storeId) {
11721
+ const baseUrl = source.endpoint.replace(/\/$/, '');
11722
+ const searchUrl = `${baseUrl}/api/v1/search`;
11723
+ const headers = {
11724
+ 'Content-Type': 'application/json',
11725
+ 'x-storeid': source.storeId,
11726
+ ...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
11727
+ };
11728
+ const response = await fetch(searchUrl, {
11729
+ method: 'POST',
11730
+ headers,
11731
+ body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
11732
+ signal,
11733
+ });
11734
+ if (!response.ok)
11735
+ return [];
11736
+ const data = await response.json();
11737
+ if (source.transformResults) {
11738
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11739
+ }
11740
+ return transformPublicSearchResults(data, source.id);
11741
+ }
11742
+ const url = new URL(source.endpoint);
11743
+ url.searchParams.set('query', query);
11744
+ url.searchParams.set('limit', String(source.maxResults || 8));
11745
+ const headers = { 'Content-Type': 'application/json' };
11746
+ if (source.apiKey)
11747
+ headers['X-Docs-API-Key'] = source.apiKey;
11748
+ const response = await fetch(url.toString(), { method: 'GET', headers, signal });
11749
+ if (!response.ok)
11750
+ return [];
11751
+ const data = await response.json();
11752
+ if (source.transformResults) {
11753
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11754
+ }
11755
+ return defaultTransform(data, source.id);
11756
+ }
11757
+ catch (error) {
11758
+ if (error instanceof Error && error.name === 'AbortError')
11759
+ throw error;
11760
+ return [];
11761
+ }
11762
+ }, [transformPublicSearchResults]);
11763
+ const fetchSuggestions = React.useCallback(async (query) => {
11764
+ if (!query.trim()) {
11765
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
11766
+ return;
11767
+ }
11768
+ abortControllersRef.current.forEach(c => c.abort());
11769
+ abortControllersRef.current.clear();
11770
+ dispatch({ type: 'SET_LOADING', payload: true });
11771
+ dispatch({ type: 'SET_ERROR', payload: null });
11772
+ try {
11773
+ const results = await Promise.all(searchSources.map(async (source) => {
11774
+ const controller = new AbortController();
11775
+ abortControllersRef.current.set(source.id, controller);
11776
+ const items = await fetchFromSource(source, query, controller.signal);
11777
+ return { source, items };
11778
+ }));
11779
+ const groupedResults = results.filter(r => r.items.length > 0);
11780
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
11781
+ }
11782
+ catch (error) {
11783
+ if (error instanceof Error && error.name === 'AbortError')
11784
+ return;
11785
+ dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
11786
+ }
11787
+ finally {
11788
+ dispatch({ type: 'SET_LOADING', payload: false });
11789
+ }
11790
+ }, [searchSources, fetchFromSource]);
11791
+ const search = React.useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
11792
+ const setQuery = React.useCallback((query) => {
11793
+ dispatch({ type: 'SET_QUERY', payload: query });
11794
+ if (debounceTimerRef.current)
11795
+ clearTimeout(debounceTimerRef.current);
11796
+ debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
11797
+ }, [fetchSuggestions, debounceMs]);
11798
+ const selectNext = React.useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
11799
+ const selectPrev = React.useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
11800
+ const setSelectedIndex = React.useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
11801
+ const reset = React.useCallback(() => {
11802
+ abortControllersRef.current.forEach(c => c.abort());
11803
+ abortControllersRef.current.clear();
11804
+ if (debounceTimerRef.current)
11805
+ clearTimeout(debounceTimerRef.current);
11806
+ dispatch({ type: 'RESET' });
11807
+ }, []);
11808
+ const getSelectedItem = React.useCallback(() => {
11809
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11810
+ return items[state.selectedIndex] || null;
11811
+ }, [state.mode, state.results, state.suggestions, state.selectedIndex]);
11812
+ React.useEffect(() => {
11813
+ return () => {
11814
+ abortControllersRef.current.forEach(c => c.abort());
11815
+ abortControllersRef.current.clear();
11816
+ if (debounceTimerRef.current)
11817
+ clearTimeout(debounceTimerRef.current);
11818
+ };
11819
+ }, []);
11820
+ return {
11821
+ ...state,
11822
+ sources: searchSources,
11823
+ setQuery,
11824
+ search,
11825
+ fetchSuggestions,
11826
+ selectNext,
11827
+ selectPrev,
11828
+ setSelectedIndex,
11829
+ reset,
11830
+ getSelectedItem,
11831
+ };
11832
+ }
11833
+
11834
+ function transformResults(results) {
11835
+ return results.map((result) => {
11836
+ const url = result.url || result.route || result.link || '';
11837
+ const title = result.title || result.name || '';
11838
+ const content = result.content || result.description || result.snippet || '';
11839
+ const description = result.description || result.content?.substring?.(0, 150) || '';
11840
+ const hierarchy = {};
11841
+ if (result.hierarchy) {
11842
+ hierarchy.lvl0 = result.hierarchy.lvl0;
11843
+ hierarchy.lvl1 = result.hierarchy.lvl1;
11844
+ hierarchy.lvl2 = result.hierarchy.lvl2;
11845
+ }
11846
+ else {
11847
+ if (result.category)
11848
+ hierarchy.lvl0 = result.category;
11849
+ if (result.parent_title || result.parentTitle)
11850
+ hierarchy.lvl1 = result.parent_title || result.parentTitle;
11851
+ }
11852
+ return {
11853
+ url,
11854
+ title: title?.replace?.(/<\/?mark>/g, '') || title,
11855
+ content: content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 200) || content,
11856
+ description: description?.replace?.(/<\/?mark>/g, '') || description,
11857
+ category: result.category || hierarchy.lvl0 || '',
11858
+ hierarchy,
11859
+ route: result.route,
11860
+ parentTitle: result.parent_title || result.parentTitle,
11861
+ type: result.type || '',
11862
+ anchor: result.anchor || '',
11863
+ _source: 'seekora',
11864
+ };
11865
+ });
11866
+ }
11867
+ function useSeekoraSearch$1(options) {
11868
+ const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField, groupSize, } = options;
11869
+ const [query, setQueryState] = React.useState('');
11870
+ const [suggestions, setSuggestions] = React.useState([]);
11871
+ const [isLoading, setIsLoading] = React.useState(false);
11872
+ const [error, setError] = React.useState(null);
11873
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
11874
+ const clientRef = React.useRef(null);
11875
+ const lastSearchContextRef = React.useRef(null);
11876
+ const debounceTimerRef = React.useRef(null);
11877
+ const abortControllerRef = React.useRef(null);
11878
+ React.useEffect(() => {
11879
+ if (!storeId)
11880
+ return;
11881
+ const config = {
11882
+ storeId,
11883
+ readSecret: storeSecret,
11884
+ logLevel: 'warn',
11885
+ enableContextCollection: true,
11886
+ };
11887
+ if (apiEndpoint) {
11888
+ if (['local', 'stage', 'production'].includes(apiEndpoint)) {
11889
+ config.environment = apiEndpoint;
11890
+ }
11891
+ else {
11892
+ config.baseUrl = apiEndpoint;
11893
+ }
11894
+ }
11895
+ try {
11896
+ clientRef.current = new searchSdk.SeekoraClient(config);
11897
+ }
11898
+ catch (err) {
11899
+ console.error('Failed to initialize SeekoraClient:', err);
11900
+ setError('Failed to initialize search client');
11901
+ }
11902
+ return () => {
11903
+ clientRef.current = null;
11904
+ };
11905
+ }, [storeId, storeSecret, apiEndpoint]);
11906
+ const performSearch = React.useCallback(async (searchQuery) => {
11907
+ if (!clientRef.current) {
11908
+ setError('Search client not initialized');
11909
+ return;
11910
+ }
11911
+ if (!searchQuery.trim()) {
11912
+ setSuggestions([]);
11913
+ return;
11914
+ }
11915
+ if (abortControllerRef.current)
11916
+ abortControllerRef.current.abort();
11917
+ abortControllerRef.current = new AbortController();
11918
+ setIsLoading(true);
11919
+ setError(null);
11920
+ try {
11921
+ const response = await clientRef.current.search(searchQuery, {
11922
+ per_page: maxResults,
11923
+ analytics_tags: analyticsTags,
11924
+ return_fields: [
11925
+ 'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
11926
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
11927
+ 'content', 'type', 'url', 'title', 'anchor'
11928
+ ],
11929
+ snippet_fields: [
11930
+ 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
11931
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
11932
+ ],
11933
+ snippet_prefix: '<mark>',
11934
+ snippet_suffix: '</mark>',
11935
+ include_snippets: true,
11936
+ group_field: groupField,
11937
+ group_size: groupSize,
11938
+ });
11939
+ if (abortControllerRef.current?.signal.aborted)
11940
+ return;
11941
+ if (response?.context)
11942
+ lastSearchContextRef.current = response.context;
11943
+ setSuggestions(transformResults(response.results || []));
11944
+ setSelectedIndex(0);
11945
+ }
11946
+ catch (err) {
11947
+ if (err instanceof Error && err.name === 'AbortError')
11948
+ return;
11949
+ console.error('Search failed:', err);
11950
+ setError(err instanceof Error ? err.message : 'Search failed');
11951
+ setSuggestions([]);
11952
+ }
11953
+ finally {
11954
+ setIsLoading(false);
11955
+ }
11956
+ }, [maxResults, analyticsTags, groupField, groupSize]);
11957
+ const setQuery = React.useCallback((newQuery) => {
11958
+ setQueryState(newQuery);
11959
+ setSelectedIndex(0);
11960
+ if (debounceTimerRef.current)
11961
+ clearTimeout(debounceTimerRef.current);
11962
+ debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
11963
+ }, [performSearch, debounceMs]);
11964
+ const selectNext = React.useCallback(() => {
11965
+ setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
11966
+ }, [suggestions.length]);
11967
+ const selectPrev = React.useCallback(() => {
11968
+ setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
11969
+ }, [suggestions.length]);
11970
+ const reset = React.useCallback(() => {
11971
+ if (abortControllerRef.current)
11972
+ abortControllerRef.current.abort();
11973
+ if (debounceTimerRef.current)
11974
+ clearTimeout(debounceTimerRef.current);
11975
+ setQueryState('');
11976
+ setSuggestions([]);
11977
+ setIsLoading(false);
11978
+ setError(null);
11979
+ setSelectedIndex(0);
11980
+ }, []);
11981
+ const getSelectedItem = React.useCallback(() => {
11982
+ return suggestions[selectedIndex] || null;
11983
+ }, [suggestions, selectedIndex]);
11984
+ const trackDocClick = React.useCallback((hit, position) => {
11985
+ const client = clientRef.current;
11986
+ if (!client?.trackEvent)
11987
+ return;
11988
+ const context = lastSearchContextRef.current ?? undefined;
11989
+ const itemId = hit.url || hit.id || hit.title || String(position);
11990
+ client.trackEvent({
11991
+ event_name: 'doc_click',
11992
+ clicked_item_id: itemId,
11993
+ metadata: { position, result: hit, source: 'docsearch' },
11994
+ }, context);
11995
+ }, []);
11996
+ React.useEffect(() => {
11997
+ return () => {
11998
+ if (abortControllerRef.current)
11999
+ abortControllerRef.current.abort();
12000
+ if (debounceTimerRef.current)
12001
+ clearTimeout(debounceTimerRef.current);
12002
+ };
12003
+ }, []);
12004
+ return {
12005
+ query,
12006
+ suggestions,
12007
+ isLoading,
12008
+ error,
12009
+ selectedIndex,
12010
+ setQuery,
12011
+ selectNext,
12012
+ selectPrev,
12013
+ setSelectedIndex,
12014
+ reset,
12015
+ getSelectedItem,
12016
+ trackDocClick,
12017
+ };
12018
+ }
12019
+
12020
+ 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', }) {
12021
+ const [isOpen, setIsOpen] = React.useState(initialOpen);
12022
+ const useSeekoraSDK = !!storeId;
12023
+ const seekoraSearch = useSeekoraSearch$1({
12024
+ storeId: storeId || '',
12025
+ storeSecret,
12026
+ apiEndpoint: seekoraApiEndpoint,
12027
+ maxResults,
12028
+ debounceMs,
12029
+ analyticsTags: ['docsearch'],
12030
+ });
12031
+ const legacySearch = useDocSearch({
12032
+ apiEndpoint,
12033
+ apiKey,
12034
+ sources,
12035
+ maxResults,
12036
+ debounceMs,
12037
+ });
12038
+ const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
12039
+ const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
12040
+ const results = useSeekoraSDK ? suggestions : legacySearch.results;
12041
+ const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
12042
+ const searchSources = useSeekoraSDK
12043
+ ? [{ id: 'seekora', name: 'Results', endpoint: '' }]
12044
+ : legacySearch.sources;
12045
+ const handleOpen = React.useCallback(() => setIsOpen(true), []);
12046
+ const handleClose = React.useCallback(() => {
12047
+ setIsOpen(false);
12048
+ reset();
12049
+ onClose?.();
12050
+ }, [reset, onClose]);
12051
+ const handleSelect = React.useCallback((hit) => {
12052
+ if (useSeekoraSDK && seekoraSearch.trackDocClick) {
12053
+ seekoraSearch.trackDocClick(hit, selectedIndex + 1);
12054
+ }
12055
+ if (onSelect) {
12056
+ onSelect(hit);
12057
+ }
12058
+ else {
12059
+ window.location.href = hit.url;
12060
+ }
12061
+ handleClose();
12062
+ }, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
12063
+ const handleEnter = React.useCallback(() => {
12064
+ const selectedItem = getSelectedItem();
12065
+ if (selectedItem)
12066
+ handleSelect(selectedItem);
12067
+ }, [getSelectedItem, handleSelect]);
12068
+ const { handleModalKeyDown } = useKeyboard({
12069
+ isOpen,
12070
+ onOpen: handleOpen,
12071
+ onClose: handleClose,
12072
+ onSelectNext: selectNext,
12073
+ onSelectPrev: selectPrev,
12074
+ onEnter: handleEnter,
12075
+ disableShortcut,
12076
+ shortcutKey,
12077
+ });
12078
+ const handleKeyDown = React.useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
12079
+ const displayHits = mode === 'results' ? results : suggestions;
12080
+ return (React.createElement(React.Fragment, null,
12081
+ renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
12082
+ React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
12083
+ React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
12084
+ React.createElement("header", { className: "seekora-docsearch-header" },
12085
+ React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
12086
+ React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
12087
+ React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
12088
+ React.createElement("div", { className: "seekora-docsearch-body" },
12089
+ React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
12090
+ React.createElement(Footer, { translations: translations })))));
12091
+ }
12092
+
10566
12093
  /**
10567
12094
  * useSeekoraSearch Hook
10568
12095
  *
@@ -10628,7 +12155,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
10628
12155
  /**
10629
12156
  * useAnalytics Hook
10630
12157
  *
10631
- * Hook for tracking analytics events with the Seekora SDK
12158
+ * Hook for tracking analytics events with the Seekora SDK.
12159
+ * Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
12160
+ * the SDK sends both legacy and v3 fields for backend compatibility.
10632
12161
  */
10633
12162
  const useAnalytics = ({ client, enabled = true, }) => {
10634
12163
  const trackEvent = React.useCallback(async (eventType, payload, context) => {
@@ -10648,14 +12177,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
10648
12177
  const trackClick = React.useCallback(async (resultId, result, context, position) => {
10649
12178
  if (!enabled)
10650
12179
  return;
10651
- await trackEvent('product_click', {
10652
- clicked_item_id: resultId,
10653
- metadata: {
10654
- result: result,
10655
- ...(position !== undefined && { position }),
10656
- },
10657
- }, context);
10658
- }, [trackEvent, enabled]);
12180
+ const pos = position ?? 0;
12181
+ if (client.trackClick) {
12182
+ await client.trackClick(resultId, pos, context);
12183
+ }
12184
+ else {
12185
+ await trackEvent('product_click', {
12186
+ clicked_item_id: resultId,
12187
+ metadata: { result, ...(position !== undefined && { position: pos }) },
12188
+ }, context);
12189
+ }
12190
+ }, [client, trackEvent, enabled]);
10659
12191
  const trackConversion = React.useCallback(async (resultId, result, value, currency, context) => {
10660
12192
  if (!enabled)
10661
12193
  return;
@@ -11681,12 +13213,15 @@ exports.AmazonDropdown = AmazonDropdown;
11681
13213
  exports.Breadcrumb = Breadcrumb;
11682
13214
  exports.ClearRefinements = ClearRefinements;
11683
13215
  exports.CurrentRefinements = CurrentRefinements;
13216
+ exports.DocSearch = DocSearch;
13217
+ exports.DocSearchButton = DocSearchButton;
11684
13218
  exports.Facets = Facets;
11685
13219
  exports.FederatedDropdown = FederatedDropdown;
13220
+ exports.Fingerprint = Fingerprint;
11686
13221
  exports.FrequentlyBoughtTogether = FrequentlyBoughtTogether;
11687
13222
  exports.GoogleDropdown = GoogleDropdown;
11688
13223
  exports.HierarchicalMenu = HierarchicalMenu;
11689
- exports.Highlight = Highlight;
13224
+ exports.Highlight = Highlight$1;
11690
13225
  exports.HitsPerPage = HitsPerPage;
11691
13226
  exports.InfiniteHits = InfiniteHits;
11692
13227
  exports.MinimalDropdown = MinimalDropdown;
@@ -11734,7 +13269,9 @@ exports.extractSuggestion = extractSuggestion;
11734
13269
  exports.formatParsedFilters = formatParsedFilters;
11735
13270
  exports.formatSuggestionPrice = formatPrice;
11736
13271
  exports.generateSuggestionsStylesheet = generateSuggestionsStylesheet;
13272
+ exports.getFingerprint = getFingerprint;
11737
13273
  exports.getRecentSearches = getRecentSearches;
13274
+ exports.getShortcutText = getShortcutText;
11738
13275
  exports.getSuggestionsCache = getSuggestionsCache;
11739
13276
  exports.highlightText = highlightText;
11740
13277
  exports.injectGlobalResponsiveStyles = injectGlobalResponsiveStyles;
@@ -11748,7 +13285,10 @@ exports.removeRecentSearch = removeRecentSearch;
11748
13285
  exports.touchTargets = touchTargets;
11749
13286
  exports.updateSuggestionsStyles = updateSuggestionsStyles;
11750
13287
  exports.useAnalytics = useAnalytics;
13288
+ exports.useDocSearch = useDocSearch;
13289
+ exports.useDocSearchSeekoraSearch = useSeekoraSearch$1;
11751
13290
  exports.useInjectResponsiveStyles = useInjectResponsiveStyles;
13291
+ exports.useKeyboard = useKeyboard;
11752
13292
  exports.useNaturalLanguageFilters = useNaturalLanguageFilters;
11753
13293
  exports.useQuerySuggestions = useQuerySuggestions;
11754
13294
  exports.useQuerySuggestionsEnhanced = useQuerySuggestionsEnhanced;