@seekora-ai/ui-sdk-react 0.1.1 → 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
@@ -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
@@ -5587,7 +6237,7 @@ const EVENTS = {
5587
6237
  // Hook Implementation
5588
6238
  // ============================================================================
5589
6239
  function useSuggestionsAnalytics(options) {
5590
- const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, } = options;
6240
+ const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, context: contextOption, } = options;
5591
6241
  // Refs for debouncing and tracking
5592
6242
  const impressionTimerRef = useRef(null);
5593
6243
  const lastImpressionRef = useRef(null);
@@ -5600,10 +6250,11 @@ function useSuggestionsAnalytics(options) {
5600
6250
  }
5601
6251
  };
5602
6252
  }, []);
5603
- // Helper to send event
5604
- const sendEvent = useCallback(async (eventName, metadata) => {
6253
+ // Helper to send event (optional context links event to search for v3 analytics)
6254
+ const sendEvent = useCallback(async (eventName, metadata, context) => {
5605
6255
  if (!enabled || !client)
5606
6256
  return;
6257
+ const searchContext = context ?? contextOption;
5607
6258
  try {
5608
6259
  await client.trackEvent?.({
5609
6260
  event_name: eventName,
@@ -5613,13 +6264,13 @@ function useSuggestionsAnalytics(options) {
5613
6264
  timestamp: Date.now(),
5614
6265
  source: 'suggestions_dropdown',
5615
6266
  },
5616
- });
6267
+ }, searchContext);
5617
6268
  log.verbose(`Analytics: ${eventName}`, metadata);
5618
6269
  }
5619
6270
  catch (error) {
5620
6271
  log.warn(`Failed to track ${eventName}`, { error });
5621
6272
  }
5622
- }, [client, enabled, analyticsTags]);
6273
+ }, [client, enabled, analyticsTags, contextOption]);
5623
6274
  // Track suggestion click
5624
6275
  const trackSuggestionClick = useCallback((data) => {
5625
6276
  if (!trackClicks)
@@ -5647,11 +6298,12 @@ function useSuggestionsAnalytics(options) {
5647
6298
  tab_id: data.tabId,
5648
6299
  original_query: data.query,
5649
6300
  });
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(() => { });
6301
+ // Also track as a general product click for analytics (with context when available)
6302
+ const searchContext = contextOption;
6303
+ if (client?.trackClick) {
6304
+ Promise.resolve(client.trackClick(data.product.id || data.product.objectID || '', data.position + 1, searchContext)).catch(() => { });
5653
6305
  }
5654
- }, [client, sendEvent, trackClicks]);
6306
+ }, [client, contextOption, sendEvent, trackClicks]);
5655
6307
  // Track category click
5656
6308
  const trackCategoryClick = useCallback((category, query) => {
5657
6309
  if (!trackClicks)
@@ -10561,6 +11213,881 @@ const SuggestionDropdownVariants = {
10561
11213
  minimal: MinimalDropdown,
10562
11214
  };
10563
11215
 
11216
+ function Modal({ isOpen, onClose, children }) {
11217
+ const overlayRef = useRef(null);
11218
+ const containerRef = useRef(null);
11219
+ useEffect(() => {
11220
+ const handleClickOutside = (event) => {
11221
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
11222
+ onClose();
11223
+ }
11224
+ };
11225
+ if (isOpen)
11226
+ document.addEventListener('mousedown', handleClickOutside);
11227
+ return () => document.removeEventListener('mousedown', handleClickOutside);
11228
+ }, [isOpen, onClose]);
11229
+ useEffect(() => {
11230
+ if (isOpen) {
11231
+ const originalOverflow = document.body.style.overflow;
11232
+ document.body.style.overflow = 'hidden';
11233
+ return () => { document.body.style.overflow = originalOverflow; };
11234
+ }
11235
+ }, [isOpen]);
11236
+ useEffect(() => {
11237
+ if (!isOpen || !containerRef.current)
11238
+ return;
11239
+ const container = containerRef.current;
11240
+ const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
11241
+ const firstElement = focusableElements[0];
11242
+ const lastElement = focusableElements[focusableElements.length - 1];
11243
+ const handleTabKey = (e) => {
11244
+ if (e.key !== 'Tab')
11245
+ return;
11246
+ if (e.shiftKey) {
11247
+ if (document.activeElement === firstElement) {
11248
+ e.preventDefault();
11249
+ lastElement?.focus();
11250
+ }
11251
+ }
11252
+ else {
11253
+ if (document.activeElement === lastElement) {
11254
+ e.preventDefault();
11255
+ firstElement?.focus();
11256
+ }
11257
+ }
11258
+ };
11259
+ container.addEventListener('keydown', handleTabKey);
11260
+ return () => container.removeEventListener('keydown', handleTabKey);
11261
+ }, [isOpen]);
11262
+ if (!isOpen || typeof document === 'undefined')
11263
+ return null;
11264
+ const modalContent = (React.createElement("div", { ref: overlayRef, className: "seekora-docsearch-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Search documentation" },
11265
+ React.createElement("div", { ref: containerRef, className: "seekora-docsearch-container" }, children)));
11266
+ return createPortal(modalContent, document.body);
11267
+ }
11268
+
11269
+ function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
11270
+ const inputRef = useRef(null);
11271
+ useEffect(() => { if (inputRef.current)
11272
+ inputRef.current.focus(); }, []);
11273
+ const handleChange = (event) => onChange(event.target.value);
11274
+ const handleClear = () => { onChange(''); onClear?.(); inputRef.current?.focus(); };
11275
+ return (React.createElement("div", { className: "seekora-docsearch-searchbox" },
11276
+ React.createElement("label", { className: "seekora-docsearch-searchbox-icon", htmlFor: "seekora-docsearch-input" }, isLoading ? (React.createElement("span", { className: "seekora-docsearch-spinner", "aria-hidden": "true" },
11277
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20" },
11278
+ React.createElement("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "40", strokeDashoffset: "10" },
11279
+ 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" },
11280
+ 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" })))),
11281
+ 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" }),
11282
+ value && (React.createElement("button", { type: "button", className: "seekora-docsearch-clear", onClick: handleClear, "aria-label": "Clear search" },
11283
+ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11284
+ 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" }))))));
11285
+ }
11286
+
11287
+ function sanitizeHtml(html) {
11288
+ const escaped = html
11289
+ .replace(/&/g, '&amp;')
11290
+ .replace(/</g, '&lt;')
11291
+ .replace(/>/g, '&gt;')
11292
+ .replace(/"/g, '&quot;')
11293
+ .replace(/'/g, '&#039;');
11294
+ return escaped
11295
+ .replace(/&lt;mark&gt;/g, '<mark>')
11296
+ .replace(/&lt;\/mark&gt;/g, '</mark>')
11297
+ .replace(/&lt;ais-highlight&gt;/g, '<mark>')
11298
+ .replace(/&lt;\/ais-highlight&gt;/g, '</mark>')
11299
+ .replace(/&lt;em&gt;/g, '<mark>')
11300
+ .replace(/&lt;\/em&gt;/g, '</mark>');
11301
+ }
11302
+ function Highlight({ value, highlightedValue }) {
11303
+ if (!highlightedValue)
11304
+ return React.createElement("span", null, value);
11305
+ return (React.createElement("span", { className: "seekora-docsearch-highlight", dangerouslySetInnerHTML: { __html: sanitizeHtml(highlightedValue) } }));
11306
+ }
11307
+ function truncateAroundMatch(content, maxLength = 150) {
11308
+ const markIndex = content.indexOf('<mark>');
11309
+ if (markIndex === -1 || content.length <= maxLength) {
11310
+ if (content.length <= maxLength)
11311
+ return content;
11312
+ return content.slice(0, maxLength) + '...';
11313
+ }
11314
+ const halfLength = Math.floor(maxLength / 2);
11315
+ let start = Math.max(0, markIndex - halfLength);
11316
+ let end = Math.min(content.length, markIndex + halfLength);
11317
+ if (start > 0) {
11318
+ const spaceIndex = content.indexOf(' ', start);
11319
+ if (spaceIndex !== -1 && spaceIndex < markIndex)
11320
+ start = spaceIndex + 1;
11321
+ }
11322
+ if (end < content.length) {
11323
+ const spaceIndex = content.lastIndexOf(' ', end);
11324
+ if (spaceIndex !== -1 && spaceIndex > markIndex)
11325
+ end = spaceIndex;
11326
+ }
11327
+ let result = content.slice(start, end);
11328
+ if (start > 0)
11329
+ result = '...' + result;
11330
+ if (end < content.length)
11331
+ result = result + '...';
11332
+ return result;
11333
+ }
11334
+
11335
+ function Hit({ hit, isSelected, onClick, onMouseEnter, openInNewTab, isChild, isLastChild, hierarchyType }) {
11336
+ const isFullHit = 'objectID' in hit;
11337
+ const suggestion = hit;
11338
+ const hitType = hierarchyType || suggestion.type;
11339
+ const breadcrumb = suggestion.parentTitle
11340
+ ? `${suggestion.category || ''} › ${suggestion.parentTitle}`.replace(/^› /, '')
11341
+ : suggestion.category || '';
11342
+ const title = getTitleForType(hit, hitType);
11343
+ let highlightedTitle = title;
11344
+ let highlightedContent = hit.content || suggestion.description || '';
11345
+ if (isFullHit) {
11346
+ const fullHit = hit;
11347
+ if (fullHit._highlightResult) {
11348
+ highlightedTitle = fullHit._highlightResult.title?.value || title;
11349
+ highlightedContent = fullHit._highlightResult.content?.value || hit.content || '';
11350
+ }
11351
+ }
11352
+ else {
11353
+ if (suggestion.highlight) {
11354
+ highlightedTitle = suggestion.highlight.title || title;
11355
+ highlightedContent = suggestion.highlight.content || hit.content || suggestion.description || '';
11356
+ }
11357
+ }
11358
+ const displayContent = highlightedContent ? truncateAroundMatch(highlightedContent, 120) : '';
11359
+ const url = hit.url || suggestion.route || '#';
11360
+ const classNames = ['seekora-docsearch-hit'];
11361
+ if (isSelected)
11362
+ classNames.push('seekora-docsearch-hit--selected');
11363
+ if (isChild)
11364
+ classNames.push('seekora-docsearch-hit--child');
11365
+ if (isLastChild)
11366
+ classNames.push('seekora-docsearch-hit--last-child');
11367
+ 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 },
11368
+ isChild && (React.createElement("div", { className: "seekora-docsearch-hit-tree" },
11369
+ React.createElement(TreeConnector, { isLast: isLastChild }))),
11370
+ React.createElement("div", { className: "seekora-docsearch-hit-icon" },
11371
+ React.createElement(HitIcon, { type: getHitTypeFromLevel(hitType) })),
11372
+ React.createElement("div", { className: "seekora-docsearch-hit-content" },
11373
+ !isChild && breadcrumb && React.createElement("span", { className: "seekora-docsearch-hit-breadcrumb" }, breadcrumb),
11374
+ React.createElement("span", { className: "seekora-docsearch-hit-title" },
11375
+ React.createElement(Highlight, { value: title, highlightedValue: highlightedTitle })),
11376
+ displayContent && (React.createElement("span", { className: "seekora-docsearch-hit-description" },
11377
+ React.createElement(Highlight, { value: hit.content || '', highlightedValue: displayContent })))),
11378
+ 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" },
11379
+ 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" },
11380
+ React.createElement("path", { d: "M6.75 3.25L11.5 8L6.75 12.75", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))))));
11381
+ }
11382
+ function getTitleForType(hit, type) {
11383
+ const hierarchy = hit.hierarchy || {};
11384
+ if (!type)
11385
+ return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
11386
+ const match = type.match(/^lvl(\d+)$/);
11387
+ if (match) {
11388
+ const level = parseInt(match[1], 10);
11389
+ const levelKey = `lvl${level}`;
11390
+ const levelTitle = hierarchy[levelKey];
11391
+ if (levelTitle)
11392
+ return levelTitle;
11393
+ }
11394
+ return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
11395
+ }
11396
+ function getHitTypeFromLevel(type) {
11397
+ if (!type)
11398
+ return 'page';
11399
+ const match = type.match(/^lvl(\d+)$/);
11400
+ if (match) {
11401
+ const level = parseInt(match[1], 10);
11402
+ if (level === 1)
11403
+ return 'page';
11404
+ if (level <= 3)
11405
+ return 'section';
11406
+ return 'content';
11407
+ }
11408
+ return 'page';
11409
+ }
11410
+ function HitIcon({ type }) {
11411
+ switch (type) {
11412
+ case 'page':
11413
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11414
+ 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" }),
11415
+ React.createElement("path", { d: "M7 7h6M7 10h6M7 13h4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11416
+ case 'section':
11417
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11418
+ React.createElement("path", { d: "M4 5.5h12M4 10h12M4 14.5h8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11419
+ case 'content':
11420
+ return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
11421
+ React.createElement("path", { d: "M4 6h12M4 10h8M4 14h10", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
11422
+ }
11423
+ }
11424
+ function TreeConnector({ isLast }) {
11425
+ 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,
11426
+ React.createElement("path", { d: "M8 0V20", stroke: "currentColor", strokeWidth: "1.5" }),
11427
+ React.createElement("path", { d: "M8 10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })))));
11428
+ }
11429
+
11430
+ function getTypeLevel(type) {
11431
+ if (!type)
11432
+ return 1;
11433
+ const match = type.match(/^lvl(\d+)$/);
11434
+ return match ? parseInt(match[1], 10) : 1;
11435
+ }
11436
+ function isChildType(type) {
11437
+ return getTypeLevel(type) >= 2;
11438
+ }
11439
+ function groupHitsByHierarchy(hits) {
11440
+ const groups = new Map();
11441
+ for (const hit of hits) {
11442
+ const lvl0 = hit.hierarchy?.lvl0 || '';
11443
+ if (!groups.has(lvl0))
11444
+ groups.set(lvl0, []);
11445
+ groups.get(lvl0).push(hit);
11446
+ }
11447
+ const result = [];
11448
+ for (const [lvl0Key, groupHits] of groups.entries()) {
11449
+ const sortedHits = [...groupHits].sort((a, b) => {
11450
+ const aType = a.type;
11451
+ const bType = b.type;
11452
+ return getTypeLevel(aType) - getTypeLevel(bType);
11453
+ });
11454
+ const markedHits = sortedHits.map((hit, index) => {
11455
+ const suggestion = hit;
11456
+ const isChild = isChildType(suggestion.type);
11457
+ const nextHit = sortedHits[index + 1];
11458
+ const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
11459
+ return { ...hit, isChild, isLastChild };
11460
+ });
11461
+ result.push({ category: lvl0Key || null, hits: markedHits });
11462
+ }
11463
+ return result;
11464
+ }
11465
+ function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
11466
+ let index = 0;
11467
+ for (let i = 0; i < groupIndex; i++)
11468
+ index += groups[i].hits.length;
11469
+ return index + hitIndex;
11470
+ }
11471
+ function getHitKey(hit, index) {
11472
+ if ('objectID' in hit)
11473
+ return hit.objectID;
11474
+ return `suggestion-${hit.url}-${index}`;
11475
+ }
11476
+ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, query, isLoading, error, translations = {}, sources: _sources = [], }) {
11477
+ const listRef = useRef(null);
11478
+ useEffect(() => {
11479
+ if (listRef.current && hits.length > 0) {
11480
+ const selectedItem = listRef.current.children[selectedIndex];
11481
+ if (selectedItem)
11482
+ selectedItem.scrollIntoView({ block: 'nearest' });
11483
+ }
11484
+ }, [selectedIndex, hits.length]);
11485
+ if (!query) {
11486
+ return (React.createElement("div", { className: "seekora-docsearch-empty" },
11487
+ React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
11488
+ }
11489
+ if (isLoading && hits.length === 0) {
11490
+ return (React.createElement("div", { className: "seekora-docsearch-loading" },
11491
+ React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
11492
+ React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
11493
+ React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
11494
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
11495
+ React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
11496
+ }
11497
+ if (error) {
11498
+ return (React.createElement("div", { className: "seekora-docsearch-error" },
11499
+ React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
11500
+ }
11501
+ if (hits.length === 0 && query) {
11502
+ return (React.createElement("div", { className: "seekora-docsearch-no-results" },
11503
+ React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
11504
+ }
11505
+ const displayGroups = groupedHits && groupedHits.length > 0
11506
+ ? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
11507
+ : groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
11508
+ return (React.createElement("div", { className: "seekora-docsearch-results" },
11509
+ 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" },
11510
+ group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
11511
+ React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
11512
+ const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
11513
+ const extHit = hit;
11514
+ return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
11515
+ 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 })));
11516
+ }))))))));
11517
+ }
11518
+
11519
+ function Footer({ translations = {} }) {
11520
+ return (React.createElement("footer", { className: "seekora-docsearch-footer" },
11521
+ React.createElement("div", { className: "seekora-docsearch-footer-commands" },
11522
+ React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
11523
+ React.createElement("li", null,
11524
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11525
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
11526
+ React.createElement("span", null, "to select"))),
11527
+ React.createElement("li", null,
11528
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11529
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
11530
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
11531
+ React.createElement("span", null, "to navigate"))),
11532
+ React.createElement("li", null,
11533
+ React.createElement("span", { className: "seekora-docsearch-footer-command" },
11534
+ React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
11535
+ React.createElement("span", null, translations.closeText || 'to close'))))),
11536
+ React.createElement("div", { className: "seekora-docsearch-footer-logo" },
11537
+ React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
11538
+ React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
11539
+ React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
11540
+ }
11541
+
11542
+ function useKeyboard(options) {
11543
+ const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
11544
+ const handleGlobalKeyDown = useCallback((event) => {
11545
+ if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
11546
+ event.preventDefault();
11547
+ if (isOpen) {
11548
+ onClose();
11549
+ }
11550
+ else {
11551
+ onOpen();
11552
+ }
11553
+ return;
11554
+ }
11555
+ if (!disableShortcut && event.key === '/' && !isOpen) {
11556
+ const target = event.target;
11557
+ const isInput = target.tagName === 'INPUT' ||
11558
+ target.tagName === 'TEXTAREA' ||
11559
+ target.isContentEditable;
11560
+ if (!isInput) {
11561
+ event.preventDefault();
11562
+ onOpen();
11563
+ }
11564
+ }
11565
+ }, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
11566
+ const handleModalKeyDown = useCallback((event) => {
11567
+ switch (event.key) {
11568
+ case 'Escape':
11569
+ event.preventDefault();
11570
+ onClose();
11571
+ break;
11572
+ case 'ArrowDown':
11573
+ event.preventDefault();
11574
+ onSelectNext();
11575
+ break;
11576
+ case 'ArrowUp':
11577
+ event.preventDefault();
11578
+ onSelectPrev();
11579
+ break;
11580
+ case 'Enter':
11581
+ event.preventDefault();
11582
+ onEnter();
11583
+ break;
11584
+ case 'Tab':
11585
+ if (event.shiftKey) {
11586
+ onSelectPrev();
11587
+ }
11588
+ else {
11589
+ onSelectNext();
11590
+ }
11591
+ event.preventDefault();
11592
+ break;
11593
+ }
11594
+ }, [onClose, onSelectNext, onSelectPrev, onEnter]);
11595
+ useEffect(() => {
11596
+ document.addEventListener('keydown', handleGlobalKeyDown);
11597
+ return () => {
11598
+ document.removeEventListener('keydown', handleGlobalKeyDown);
11599
+ };
11600
+ }, [handleGlobalKeyDown]);
11601
+ return {
11602
+ handleModalKeyDown,
11603
+ };
11604
+ }
11605
+ function getShortcutText(key = 'K') {
11606
+ if (typeof navigator === 'undefined') {
11607
+ return `⌘${key}`;
11608
+ }
11609
+ const isMac = navigator.platform.toLowerCase().includes('mac');
11610
+ return isMac ? `⌘${key}` : `Ctrl+${key}`;
11611
+ }
11612
+
11613
+ function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
11614
+ const shortcutText = getShortcutText('K');
11615
+ return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
11616
+ React.createElement("span", { className: "seekora-docsearch-button-icon" },
11617
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
11618
+ 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" }))),
11619
+ React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
11620
+ React.createElement("span", { className: "seekora-docsearch-button-keys" },
11621
+ React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
11622
+ }
11623
+
11624
+ const initialState = {
11625
+ query: '',
11626
+ results: [],
11627
+ suggestions: [],
11628
+ groupedSuggestions: [],
11629
+ isLoading: false,
11630
+ error: null,
11631
+ selectedIndex: 0,
11632
+ mode: 'suggestions',
11633
+ };
11634
+ function reducer(state, action) {
11635
+ switch (action.type) {
11636
+ case 'SET_QUERY':
11637
+ return { ...state, query: action.payload, selectedIndex: 0 };
11638
+ case 'SET_RESULTS':
11639
+ return { ...state, results: action.payload, mode: 'results' };
11640
+ case 'SET_SUGGESTIONS':
11641
+ return { ...state, suggestions: action.payload, mode: 'suggestions' };
11642
+ case 'SET_GROUPED_SUGGESTIONS': {
11643
+ const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
11644
+ return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
11645
+ }
11646
+ case 'SET_LOADING':
11647
+ return { ...state, isLoading: action.payload };
11648
+ case 'SET_ERROR':
11649
+ return { ...state, error: action.payload };
11650
+ case 'SET_SELECTED_INDEX':
11651
+ return { ...state, selectedIndex: action.payload };
11652
+ case 'SELECT_NEXT': {
11653
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11654
+ const maxIndex = items.length - 1;
11655
+ return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
11656
+ }
11657
+ case 'SELECT_PREV': {
11658
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11659
+ const maxIndex = items.length - 1;
11660
+ return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
11661
+ }
11662
+ case 'SET_MODE':
11663
+ return { ...state, mode: action.payload, selectedIndex: 0 };
11664
+ case 'RESET':
11665
+ return initialState;
11666
+ default:
11667
+ return state;
11668
+ }
11669
+ }
11670
+ function useDocSearch(options) {
11671
+ const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200 } = options;
11672
+ const searchSources = sources || (apiEndpoint ? [{
11673
+ id: 'default',
11674
+ name: 'Results',
11675
+ endpoint: apiEndpoint,
11676
+ apiKey,
11677
+ maxResults,
11678
+ }] : []);
11679
+ const [state, dispatch] = useReducer(reducer, initialState);
11680
+ const abortControllersRef = useRef(new Map());
11681
+ const debounceTimerRef = useRef(null);
11682
+ const defaultTransform = (data, sourceId) => {
11683
+ const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
11684
+ return items.map((item) => ({
11685
+ url: item.url || item.route || '',
11686
+ title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
11687
+ content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
11688
+ description: item.description || item.content?.substring?.(0, 100) || '',
11689
+ category: item.category || item.hierarchy?.lvl0 || '',
11690
+ hierarchy: item.hierarchy,
11691
+ route: item.route,
11692
+ parentTitle: item.parent_title || item.parentTitle,
11693
+ _source: sourceId,
11694
+ }));
11695
+ };
11696
+ const transformPublicSearchResults = useCallback((data, sourceId) => {
11697
+ const results = data?.data?.results || [];
11698
+ return results.map((item) => {
11699
+ const doc = item.document || item;
11700
+ return {
11701
+ url: doc.url || doc.route || '',
11702
+ title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
11703
+ content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
11704
+ description: doc.description || doc.content?.substring?.(0, 100) || '',
11705
+ category: doc.category || doc.hierarchy?.lvl0 || '',
11706
+ hierarchy: doc.hierarchy,
11707
+ route: doc.route,
11708
+ parentTitle: doc.parent_title || doc.parentTitle,
11709
+ _source: sourceId,
11710
+ };
11711
+ });
11712
+ }, []);
11713
+ const fetchFromSource = useCallback(async (source, query, signal) => {
11714
+ const minLength = source.minQueryLength ?? 1;
11715
+ if (query.length < minLength)
11716
+ return [];
11717
+ try {
11718
+ if (source.storeId) {
11719
+ const baseUrl = source.endpoint.replace(/\/$/, '');
11720
+ const searchUrl = `${baseUrl}/api/v1/search`;
11721
+ const headers = {
11722
+ 'Content-Type': 'application/json',
11723
+ 'x-storeid': source.storeId,
11724
+ ...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
11725
+ };
11726
+ const response = await fetch(searchUrl, {
11727
+ method: 'POST',
11728
+ headers,
11729
+ body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
11730
+ signal,
11731
+ });
11732
+ if (!response.ok)
11733
+ return [];
11734
+ const data = await response.json();
11735
+ if (source.transformResults) {
11736
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11737
+ }
11738
+ return transformPublicSearchResults(data, source.id);
11739
+ }
11740
+ const url = new URL(source.endpoint);
11741
+ url.searchParams.set('query', query);
11742
+ url.searchParams.set('limit', String(source.maxResults || 8));
11743
+ const headers = { 'Content-Type': 'application/json' };
11744
+ if (source.apiKey)
11745
+ headers['X-Docs-API-Key'] = source.apiKey;
11746
+ const response = await fetch(url.toString(), { method: 'GET', headers, signal });
11747
+ if (!response.ok)
11748
+ return [];
11749
+ const data = await response.json();
11750
+ if (source.transformResults) {
11751
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
11752
+ }
11753
+ return defaultTransform(data, source.id);
11754
+ }
11755
+ catch (error) {
11756
+ if (error instanceof Error && error.name === 'AbortError')
11757
+ throw error;
11758
+ return [];
11759
+ }
11760
+ }, [transformPublicSearchResults]);
11761
+ const fetchSuggestions = useCallback(async (query) => {
11762
+ if (!query.trim()) {
11763
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
11764
+ return;
11765
+ }
11766
+ abortControllersRef.current.forEach(c => c.abort());
11767
+ abortControllersRef.current.clear();
11768
+ dispatch({ type: 'SET_LOADING', payload: true });
11769
+ dispatch({ type: 'SET_ERROR', payload: null });
11770
+ try {
11771
+ const results = await Promise.all(searchSources.map(async (source) => {
11772
+ const controller = new AbortController();
11773
+ abortControllersRef.current.set(source.id, controller);
11774
+ const items = await fetchFromSource(source, query, controller.signal);
11775
+ return { source, items };
11776
+ }));
11777
+ const groupedResults = results.filter(r => r.items.length > 0);
11778
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
11779
+ }
11780
+ catch (error) {
11781
+ if (error instanceof Error && error.name === 'AbortError')
11782
+ return;
11783
+ dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
11784
+ }
11785
+ finally {
11786
+ dispatch({ type: 'SET_LOADING', payload: false });
11787
+ }
11788
+ }, [searchSources, fetchFromSource]);
11789
+ const search = useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
11790
+ const setQuery = useCallback((query) => {
11791
+ dispatch({ type: 'SET_QUERY', payload: query });
11792
+ if (debounceTimerRef.current)
11793
+ clearTimeout(debounceTimerRef.current);
11794
+ debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
11795
+ }, [fetchSuggestions, debounceMs]);
11796
+ const selectNext = useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
11797
+ const selectPrev = useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
11798
+ const setSelectedIndex = useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
11799
+ const reset = useCallback(() => {
11800
+ abortControllersRef.current.forEach(c => c.abort());
11801
+ abortControllersRef.current.clear();
11802
+ if (debounceTimerRef.current)
11803
+ clearTimeout(debounceTimerRef.current);
11804
+ dispatch({ type: 'RESET' });
11805
+ }, []);
11806
+ const getSelectedItem = useCallback(() => {
11807
+ const items = state.mode === 'results' ? state.results : state.suggestions;
11808
+ return items[state.selectedIndex] || null;
11809
+ }, [state.mode, state.results, state.suggestions, state.selectedIndex]);
11810
+ useEffect(() => {
11811
+ return () => {
11812
+ abortControllersRef.current.forEach(c => c.abort());
11813
+ abortControllersRef.current.clear();
11814
+ if (debounceTimerRef.current)
11815
+ clearTimeout(debounceTimerRef.current);
11816
+ };
11817
+ }, []);
11818
+ return {
11819
+ ...state,
11820
+ sources: searchSources,
11821
+ setQuery,
11822
+ search,
11823
+ fetchSuggestions,
11824
+ selectNext,
11825
+ selectPrev,
11826
+ setSelectedIndex,
11827
+ reset,
11828
+ getSelectedItem,
11829
+ };
11830
+ }
11831
+
11832
+ function transformResults(results) {
11833
+ return results.map((result) => {
11834
+ const url = result.url || result.route || result.link || '';
11835
+ const title = result.title || result.name || '';
11836
+ const content = result.content || result.description || result.snippet || '';
11837
+ const description = result.description || result.content?.substring?.(0, 150) || '';
11838
+ const hierarchy = {};
11839
+ if (result.hierarchy) {
11840
+ hierarchy.lvl0 = result.hierarchy.lvl0;
11841
+ hierarchy.lvl1 = result.hierarchy.lvl1;
11842
+ hierarchy.lvl2 = result.hierarchy.lvl2;
11843
+ }
11844
+ else {
11845
+ if (result.category)
11846
+ hierarchy.lvl0 = result.category;
11847
+ if (result.parent_title || result.parentTitle)
11848
+ hierarchy.lvl1 = result.parent_title || result.parentTitle;
11849
+ }
11850
+ return {
11851
+ url,
11852
+ title: title?.replace?.(/<\/?mark>/g, '') || title,
11853
+ content: content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 200) || content,
11854
+ description: description?.replace?.(/<\/?mark>/g, '') || description,
11855
+ category: result.category || hierarchy.lvl0 || '',
11856
+ hierarchy,
11857
+ route: result.route,
11858
+ parentTitle: result.parent_title || result.parentTitle,
11859
+ type: result.type || '',
11860
+ anchor: result.anchor || '',
11861
+ _source: 'seekora',
11862
+ };
11863
+ });
11864
+ }
11865
+ function useSeekoraSearch$1(options) {
11866
+ const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField, groupSize, } = options;
11867
+ const [query, setQueryState] = useState('');
11868
+ const [suggestions, setSuggestions] = useState([]);
11869
+ const [isLoading, setIsLoading] = useState(false);
11870
+ const [error, setError] = useState(null);
11871
+ const [selectedIndex, setSelectedIndex] = useState(0);
11872
+ const clientRef = useRef(null);
11873
+ const lastSearchContextRef = useRef(null);
11874
+ const debounceTimerRef = useRef(null);
11875
+ const abortControllerRef = useRef(null);
11876
+ useEffect(() => {
11877
+ if (!storeId)
11878
+ return;
11879
+ const config = {
11880
+ storeId,
11881
+ readSecret: storeSecret,
11882
+ logLevel: 'warn',
11883
+ enableContextCollection: true,
11884
+ };
11885
+ if (apiEndpoint) {
11886
+ if (['local', 'stage', 'production'].includes(apiEndpoint)) {
11887
+ config.environment = apiEndpoint;
11888
+ }
11889
+ else {
11890
+ config.baseUrl = apiEndpoint;
11891
+ }
11892
+ }
11893
+ try {
11894
+ clientRef.current = new SeekoraClient(config);
11895
+ }
11896
+ catch (err) {
11897
+ console.error('Failed to initialize SeekoraClient:', err);
11898
+ setError('Failed to initialize search client');
11899
+ }
11900
+ return () => {
11901
+ clientRef.current = null;
11902
+ };
11903
+ }, [storeId, storeSecret, apiEndpoint]);
11904
+ const performSearch = useCallback(async (searchQuery) => {
11905
+ if (!clientRef.current) {
11906
+ setError('Search client not initialized');
11907
+ return;
11908
+ }
11909
+ if (!searchQuery.trim()) {
11910
+ setSuggestions([]);
11911
+ return;
11912
+ }
11913
+ if (abortControllerRef.current)
11914
+ abortControllerRef.current.abort();
11915
+ abortControllerRef.current = new AbortController();
11916
+ setIsLoading(true);
11917
+ setError(null);
11918
+ try {
11919
+ const response = await clientRef.current.search(searchQuery, {
11920
+ per_page: maxResults,
11921
+ analytics_tags: analyticsTags,
11922
+ return_fields: [
11923
+ 'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
11924
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
11925
+ 'content', 'type', 'url', 'title', 'anchor'
11926
+ ],
11927
+ snippet_fields: [
11928
+ 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
11929
+ 'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
11930
+ ],
11931
+ snippet_prefix: '<mark>',
11932
+ snippet_suffix: '</mark>',
11933
+ include_snippets: true,
11934
+ group_field: groupField,
11935
+ group_size: groupSize,
11936
+ });
11937
+ if (abortControllerRef.current?.signal.aborted)
11938
+ return;
11939
+ if (response?.context)
11940
+ lastSearchContextRef.current = response.context;
11941
+ setSuggestions(transformResults(response.results || []));
11942
+ setSelectedIndex(0);
11943
+ }
11944
+ catch (err) {
11945
+ if (err instanceof Error && err.name === 'AbortError')
11946
+ return;
11947
+ console.error('Search failed:', err);
11948
+ setError(err instanceof Error ? err.message : 'Search failed');
11949
+ setSuggestions([]);
11950
+ }
11951
+ finally {
11952
+ setIsLoading(false);
11953
+ }
11954
+ }, [maxResults, analyticsTags, groupField, groupSize]);
11955
+ const setQuery = useCallback((newQuery) => {
11956
+ setQueryState(newQuery);
11957
+ setSelectedIndex(0);
11958
+ if (debounceTimerRef.current)
11959
+ clearTimeout(debounceTimerRef.current);
11960
+ debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
11961
+ }, [performSearch, debounceMs]);
11962
+ const selectNext = useCallback(() => {
11963
+ setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
11964
+ }, [suggestions.length]);
11965
+ const selectPrev = useCallback(() => {
11966
+ setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
11967
+ }, [suggestions.length]);
11968
+ const reset = useCallback(() => {
11969
+ if (abortControllerRef.current)
11970
+ abortControllerRef.current.abort();
11971
+ if (debounceTimerRef.current)
11972
+ clearTimeout(debounceTimerRef.current);
11973
+ setQueryState('');
11974
+ setSuggestions([]);
11975
+ setIsLoading(false);
11976
+ setError(null);
11977
+ setSelectedIndex(0);
11978
+ }, []);
11979
+ const getSelectedItem = useCallback(() => {
11980
+ return suggestions[selectedIndex] || null;
11981
+ }, [suggestions, selectedIndex]);
11982
+ const trackDocClick = useCallback((hit, position) => {
11983
+ const client = clientRef.current;
11984
+ if (!client?.trackEvent)
11985
+ return;
11986
+ const context = lastSearchContextRef.current ?? undefined;
11987
+ const itemId = hit.url || hit.id || hit.title || String(position);
11988
+ client.trackEvent({
11989
+ event_name: 'doc_click',
11990
+ clicked_item_id: itemId,
11991
+ metadata: { position, result: hit, source: 'docsearch' },
11992
+ }, context);
11993
+ }, []);
11994
+ useEffect(() => {
11995
+ return () => {
11996
+ if (abortControllerRef.current)
11997
+ abortControllerRef.current.abort();
11998
+ if (debounceTimerRef.current)
11999
+ clearTimeout(debounceTimerRef.current);
12000
+ };
12001
+ }, []);
12002
+ return {
12003
+ query,
12004
+ suggestions,
12005
+ isLoading,
12006
+ error,
12007
+ selectedIndex,
12008
+ setQuery,
12009
+ selectNext,
12010
+ selectPrev,
12011
+ setSelectedIndex,
12012
+ reset,
12013
+ getSelectedItem,
12014
+ trackDocClick,
12015
+ };
12016
+ }
12017
+
12018
+ 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', }) {
12019
+ const [isOpen, setIsOpen] = useState(initialOpen);
12020
+ const useSeekoraSDK = !!storeId;
12021
+ const seekoraSearch = useSeekoraSearch$1({
12022
+ storeId: storeId || '',
12023
+ storeSecret,
12024
+ apiEndpoint: seekoraApiEndpoint,
12025
+ maxResults,
12026
+ debounceMs,
12027
+ analyticsTags: ['docsearch'],
12028
+ });
12029
+ const legacySearch = useDocSearch({
12030
+ apiEndpoint,
12031
+ apiKey,
12032
+ sources,
12033
+ maxResults,
12034
+ debounceMs,
12035
+ });
12036
+ const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
12037
+ const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
12038
+ const results = useSeekoraSDK ? suggestions : legacySearch.results;
12039
+ const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
12040
+ const searchSources = useSeekoraSDK
12041
+ ? [{ id: 'seekora', name: 'Results', endpoint: '' }]
12042
+ : legacySearch.sources;
12043
+ const handleOpen = useCallback(() => setIsOpen(true), []);
12044
+ const handleClose = useCallback(() => {
12045
+ setIsOpen(false);
12046
+ reset();
12047
+ onClose?.();
12048
+ }, [reset, onClose]);
12049
+ const handleSelect = useCallback((hit) => {
12050
+ if (useSeekoraSDK && seekoraSearch.trackDocClick) {
12051
+ seekoraSearch.trackDocClick(hit, selectedIndex + 1);
12052
+ }
12053
+ if (onSelect) {
12054
+ onSelect(hit);
12055
+ }
12056
+ else {
12057
+ window.location.href = hit.url;
12058
+ }
12059
+ handleClose();
12060
+ }, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
12061
+ const handleEnter = useCallback(() => {
12062
+ const selectedItem = getSelectedItem();
12063
+ if (selectedItem)
12064
+ handleSelect(selectedItem);
12065
+ }, [getSelectedItem, handleSelect]);
12066
+ const { handleModalKeyDown } = useKeyboard({
12067
+ isOpen,
12068
+ onOpen: handleOpen,
12069
+ onClose: handleClose,
12070
+ onSelectNext: selectNext,
12071
+ onSelectPrev: selectPrev,
12072
+ onEnter: handleEnter,
12073
+ disableShortcut,
12074
+ shortcutKey,
12075
+ });
12076
+ const handleKeyDown = useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
12077
+ const displayHits = mode === 'results' ? results : suggestions;
12078
+ return (React.createElement(React.Fragment, null,
12079
+ renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
12080
+ React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
12081
+ React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
12082
+ React.createElement("header", { className: "seekora-docsearch-header" },
12083
+ React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
12084
+ React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
12085
+ React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
12086
+ React.createElement("div", { className: "seekora-docsearch-body" },
12087
+ React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
12088
+ React.createElement(Footer, { translations: translations })))));
12089
+ }
12090
+
10564
12091
  /**
10565
12092
  * useSeekoraSearch Hook
10566
12093
  *
@@ -10626,7 +12153,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
10626
12153
  /**
10627
12154
  * useAnalytics Hook
10628
12155
  *
10629
- * Hook for tracking analytics events with the Seekora SDK
12156
+ * Hook for tracking analytics events with the Seekora SDK.
12157
+ * Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
12158
+ * the SDK sends both legacy and v3 fields for backend compatibility.
10630
12159
  */
10631
12160
  const useAnalytics = ({ client, enabled = true, }) => {
10632
12161
  const trackEvent = useCallback(async (eventType, payload, context) => {
@@ -10646,14 +12175,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
10646
12175
  const trackClick = useCallback(async (resultId, result, context, position) => {
10647
12176
  if (!enabled)
10648
12177
  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]);
12178
+ const pos = position ?? 0;
12179
+ if (client.trackClick) {
12180
+ await client.trackClick(resultId, pos, context);
12181
+ }
12182
+ else {
12183
+ await trackEvent('product_click', {
12184
+ clicked_item_id: resultId,
12185
+ metadata: { result, ...(position !== undefined && { position: pos }) },
12186
+ }, context);
12187
+ }
12188
+ }, [client, trackEvent, enabled]);
10657
12189
  const trackConversion = useCallback(async (resultId, result, value, currency, context) => {
10658
12190
  if (!enabled)
10659
12191
  return;
@@ -11675,5 +13207,5 @@ function updateSuggestionsStyles(theme) {
11675
13207
  injectSuggestionsStyles(theme, true);
11676
13208
  }
11677
13209
 
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 };
13210
+ 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
13211
  //# sourceMappingURL=index.esm.js.map