bonsai-search 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1693 @@
1
+ import { marked } from "marked";
2
+
3
+ //#region sdk.css?raw
4
+ var sdk_default = "/* Bonsai Search SDK Styles\n * Ported from React prototype design system\n * All classes prefixed with bonsai- for isolation\n */\n\n/* ============================================================================\n * CSS VARIABLES - Design Tokens\n * ============================================================================ */\n:root {\n /* Colors - Default theme */\n --bonsai-brand-color: #0a5b3b;\n --bonsai-text-color: #303030;\n --bonsai-suggestions-text-color: #303030;\n --bonsai-input-text-color: #303030;\n --bonsai-results-text-color: #303030;\n --bonsai-card-text-color: #303030;\n --bonsai-muted-color: #9ca3af;\n --bonsai-input-bg: #f5f5f5;\n --bonsai-card-bg: #f5f5f5;\n --bonsai-canvas-color: #fafafa;\n --bonsai-surface-color: #ffffff;\n --bonsai-border-color: rgba(0, 0, 0, 0.06);\n --bonsai-border-color-hover: rgba(0, 0, 0, 0.12);\n --bonsai-hover-bg: rgba(0, 0, 0, 0.04);\n --bonsai-suggestions-hover-bg: rgba(0, 0, 0, 0.04);\n --bonsai-error-bg: rgba(220, 53, 69, 0.1);\n --bonsai-error-color: #c82333;\n\n /* Spacing (4px base) */\n --bonsai-space-1: 0.25rem;\n /* 4px */\n --bonsai-space-2: 0.5rem;\n /* 8px */\n --bonsai-space-3: 0.75rem;\n /* 12px */\n --bonsai-space-4: 1rem;\n /* 16px */\n --bonsai-space-5: 1.25rem;\n /* 20px */\n --bonsai-space-6: 1.5rem;\n /* 24px */\n\n /* Typography */\n --bonsai-font-heading: system-ui, -apple-system, sans-serif;\n --bonsai-font-body: system-ui, -apple-system, sans-serif;\n --bonsai-font-mono: ui-monospace, monospace;\n --bonsai-font-size-xs: 0.75rem;\n /* 12px */\n --bonsai-font-size-sm: 0.875rem;\n /* 14px */\n --bonsai-font-size-base: 1rem;\n /* 16px */\n --bonsai-font-size-lg: 1.125rem;\n /* 18px */\n --bonsai-font-size-xl: 1.25rem;\n /* 20px */\n\n /* Border Radius */\n --bonsai-radius-none: 0;\n --bonsai-radius-sm: 0.25rem;\n /* 4px */\n --bonsai-radius-md: 0.375rem;\n /* 6px */\n --bonsai-radius-lg: 0.5rem;\n /* 8px */\n --bonsai-radius-xl: 0.75rem;\n /* 12px */\n --bonsai-radius-full: 9999px;\n\n /* Animation */\n --bonsai-duration-fast: 150ms;\n --bonsai-duration-base: 200ms;\n --bonsai-duration-slow: 300ms;\n --bonsai-easing: cubic-bezier(0, 0, 0.2, 1);\n\n /* Shadows */\n --bonsai-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n --bonsai-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --bonsai-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n\n /* Component Sizes */\n --bonsai-search-min-height: 3.5rem;\n /* 56px */\n --bonsai-search-max-width: 42rem;\n /* 672px */\n --bonsai-icon-size: 1.25rem;\n /* 20px */\n --bonsai-icon-size-sm: 1rem;\n /* 16px */\n}\n\n:host([data-theme=\"dark\"]) {\n --bonsai-text-color: #e5e7eb;\n --bonsai-suggestions-text-color: #e5e7eb;\n --bonsai-input-text-color: #e5e7eb;\n --bonsai-results-text-color: #e5e7eb;\n --bonsai-card-text-color: #e5e7eb;\n --bonsai-muted-color: #a1a1aa;\n --bonsai-input-bg: #1f2229;\n --bonsai-card-bg: #1f2229;\n --bonsai-canvas-color: #0f1115;\n --bonsai-surface-color: #181a20;\n --bonsai-border-color: rgba(255, 255, 255, 0.08);\n --bonsai-border-color-hover: rgba(255, 255, 255, 0.14);\n --bonsai-hover-bg: rgba(255, 255, 255, 0.06);\n --bonsai-suggestions-hover-bg: rgba(255, 255, 255, 0.06);\n --bonsai-error-bg: rgba(248, 113, 113, 0.15);\n --bonsai-error-color: #fca5a5;\n --bonsai-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);\n --bonsai-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.55), 0 2px 4px -2px rgb(0 0 0 / 0.5);\n --bonsai-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.6), 0 4px 6px -4px rgb(0 0 0 / 0.55);\n}\n\n/* ============================================================================\n * BASE CONTAINER\n * ============================================================================ */\n.bonsai-search-container {\n max-width: var(--bonsai-search-max-width);\n font-family: var(--bonsai-font-heading);\n box-sizing: border-box;\n}\n\n.bonsai-search-container *,\n.bonsai-search-container *::before,\n.bonsai-search-container *::after {\n box-sizing: border-box;\n}\n\n.bonsai-search-wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--bonsai-space-1);\n position: relative;\n}\n\n/* ============================================================================\n * SEARCH INPUT CONTAINER\n * ============================================================================ */\n.bonsai-search-bar {\n position: relative;\n z-index: 2;\n display: flex;\n align-items: center;\n min-height: var(--bonsai-search-min-height);\n background-color: var(--bonsai-input-bg);\n border-radius: var(--bonsai-radius-lg);\n transition: box-shadow var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-search-bar.bonsai-focused {\n box-shadow: 0 0 0 2px var(--bonsai-brand-color);\n}\n\n/* ============================================================================\n * SEARCH ICON (Left side)\n * ============================================================================ */\n.bonsai-search-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n padding-left: var(--bonsai-space-4);\n padding-right: var(--bonsai-space-2);\n color: var(--bonsai-input-text-color, var(--bonsai-text-color));\n transition: color var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-search-bar:hover .bonsai-search-icon,\n.bonsai-search-bar.bonsai-focused .bonsai-search-icon {\n color: var(--bonsai-input-text-color, var(--bonsai-text-color));\n}\n\n.bonsai-search-icon svg {\n width: var(--bonsai-icon-size);\n height: var(--bonsai-icon-size);\n stroke: currentColor;\n stroke-width: 2;\n fill: none;\n}\n\n/* ============================================================================\n * INPUT FIELD\n * ============================================================================ */\n.bonsai-search-input {\n flex: 1;\n min-width: 0;\n padding: var(--bonsai-space-4) var(--bonsai-space-4) var(--bonsai-space-4) 0;\n border: none;\n background: transparent;\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-base);\n color: var(--bonsai-input-text-color, var(--bonsai-text-color));\n outline: none;\n}\n\n.bonsai-search-input::placeholder {\n color: var(--bonsai-muted-color);\n}\n\n.bonsai-search-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n}\n\n/* Webkit search clear button styling */\n.bonsai-search-input::-webkit-search-cancel-button {\n -webkit-appearance: none;\n appearance: none;\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n cursor: pointer;\n background-color: var(--bonsai-muted-color);\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'%3E%3C/path%3E%3Cpath d='M6 6l12 12'%3E%3C/path%3E%3C/svg%3E\");\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'%3E%3C/path%3E%3Cpath d='M6 6l12 12'%3E%3C/path%3E%3C/svg%3E\");\n mask-size: contain;\n -webkit-mask-size: contain;\n mask-repeat: no-repeat;\n -webkit-mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-position: center;\n opacity: 0.7;\n transition: opacity var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-search-input::-webkit-search-cancel-button:hover {\n opacity: 1;\n}\n\n/* ============================================================================\n * ACTION BUTTONS AREA (Right side)\n * ============================================================================ */\n.bonsai-search-actions {\n display: flex;\n align-items: center;\n gap: var(--bonsai-space-2);\n padding-right: var(--bonsai-space-3);\n flex: 0 0 auto;\n min-width: max-content;\n}\n\n/* Submit Button */\n.bonsai-submit-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: var(--bonsai-space-2);\n border: none;\n border-radius: var(--bonsai-radius-lg);\n background-color: var(--bonsai-brand-color);\n cursor: pointer;\n touch-action: manipulation;\n flex-shrink: 0;\n transition:\n opacity var(--bonsai-duration-fast) var(--bonsai-easing),\n transform var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-submit-btn.bonsai-visible {\n display: flex;\n}\n\n.bonsai-submit-btn:hover {\n opacity: 0.8;\n}\n\n.bonsai-submit-btn:active {\n transform: scale(0.95);\n}\n\n.bonsai-submit-btn svg {\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n stroke: white;\n stroke-width: 2;\n fill: none;\n}\n\n/* Loading State */\n.bonsai-loading-state {\n display: none;\n align-items: center;\n gap: var(--bonsai-space-2);\n flex-shrink: 0;\n}\n\n.bonsai-loading-state.bonsai-visible {\n display: flex;\n}\n\n.bonsai-loading-text {\n position: relative;\n height: 1.25rem;\n overflow: hidden;\n min-width: 5rem;\n text-align: right;\n}\n\n.bonsai-loading-text-inner {\n display: flex;\n flex-direction: column;\n transition: transform var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-loading-text span {\n display: block;\n height: 1.25rem;\n line-height: 1.25rem;\n font-family: var(--bonsai-font-mono);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-muted-color);\n white-space: nowrap;\n}\n\n/* Spinner */\n.bonsai-spinner {\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n animation: bonsai-spin 1s linear infinite;\n transform-origin: center;\n}\n\n.bonsai-spinner svg {\n display: block;\n width: 100%;\n height: 100%;\n transform-origin: center;\n}\n\n.bonsai-spinner-track {\n stroke: var(--bonsai-muted-color);\n stroke-opacity: 0.25;\n}\n\n.bonsai-spinner-fill {\n fill: var(--bonsai-brand-color);\n}\n\n@keyframes bonsai-spin {\n from {\n transform: rotate(0deg);\n }\n\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Done State */\n.bonsai-done-state {\n display: none;\n align-items: center;\n gap: var(--bonsai-space-2);\n flex-shrink: 0;\n}\n\n.bonsai-done-state.bonsai-visible {\n display: flex;\n}\n\n.bonsai-done-text {\n font-family: var(--bonsai-font-mono);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-muted-color);\n}\n\n.bonsai-done-icon svg {\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n stroke: var(--bonsai-text-color);\n stroke-width: 2;\n fill: none;\n}\n\n/* Reset/Retry Button */\n.bonsai-reset-btn {\n display: none;\n align-items: center;\n gap: var(--bonsai-space-2);\n padding: 0;\n border: none;\n background: transparent;\n cursor: pointer;\n touch-action: manipulation;\n flex-shrink: 0;\n transition: opacity var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-reset-btn.bonsai-visible {\n display: flex;\n}\n\n.bonsai-reset-btn:hover {\n opacity: 0.7;\n}\n\n.bonsai-reset-text {\n font-family: var(--bonsai-font-mono);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-muted-color);\n}\n\n.bonsai-reset-icon {\n transition: transform var(--bonsai-duration-slow) var(--bonsai-easing);\n transform-origin: center;\n}\n\n.bonsai-reset-btn:hover .bonsai-reset-icon {\n transform: rotate(-180deg);\n}\n\n.bonsai-reset-icon svg {\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n stroke: var(--bonsai-muted-color);\n stroke-width: 2;\n fill: none;\n transform-origin: center;\n display: block;\n}\n\n/* ============================================================================\n * SUGGESTIONS DROPDOWN\n * ============================================================================ */\n.bonsai-suggestions {\n position: absolute;\n top: calc(100% + var(--bonsai-space-1));\n left: 0;\n right: 0;\n background-color: var(--bonsai-input-bg);\n border-radius: var(--bonsai-radius-lg);\n box-shadow: var(--bonsai-shadow-lg);\n overflow: hidden;\n z-index: 10;\n opacity: 0;\n transform: translateY(-10px);\n pointer-events: none;\n transition:\n opacity var(--bonsai-duration-fast) var(--bonsai-easing),\n transform var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-suggestions.bonsai-visible {\n opacity: 1;\n transform: translateY(0);\n pointer-events: auto;\n}\n\n.bonsai-suggestion-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: var(--bonsai-space-2) var(--bonsai-space-4);\n border: none;\n background: transparent;\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-suggestions-text-color, var(--bonsai-text-color));\n text-align: left;\n cursor: pointer;\n transition: background-color var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-suggestion-item:hover {\n background-color: var(--bonsai-suggestions-hover-bg, var(--bonsai-hover-bg));\n}\n\n.bonsai-suggestion-arrow {\n opacity: 0;\n visibility: hidden;\n transition:\n opacity var(--bonsai-duration-fast) var(--bonsai-easing),\n visibility 0s linear var(--bonsai-duration-fast);\n}\n\n.bonsai-suggestion-item:hover .bonsai-suggestion-arrow {\n opacity: 1;\n visibility: visible;\n transition-delay: 0s;\n}\n\n.bonsai-suggestion-arrow svg {\n width: var(--bonsai-icon-size-sm);\n height: var(--bonsai-icon-size-sm);\n stroke: var(--bonsai-muted-color);\n stroke-width: 2;\n fill: none;\n}\n\n/* ============================================================================\n * POWERED BY BRANDING\n * ============================================================================ */\n.bonsai-powered-by {\n position: relative;\n z-index: 1;\n height: 1.5rem;\n margin-top: var(--bonsai-space-1);\n text-align: right;\n font-family: var(--bonsai-font-mono);\n font-size: var(--bonsai-font-size-xs);\n color: var(--bonsai-muted-color);\n opacity: 1;\n transition: opacity var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-powered-by.bonsai-hidden {\n opacity: 0;\n}\n\n.bonsai-powered-by a {\n color: var(--bonsai-brand-color);\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n gap: 0;\n transition: gap var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-powered-by a:hover {\n gap: var(--bonsai-space-1);\n}\n\n.bonsai-powered-by-dot {\n display: inline-block;\n width: 0;\n height: 0.5rem;\n border-radius: var(--bonsai-radius-full);\n background-color: var(--bonsai-brand-color);\n transform: scale(0);\n transition:\n width var(--bonsai-duration-base) var(--bonsai-easing),\n transform var(--bonsai-duration-base) var(--bonsai-easing);\n}\n\n.bonsai-powered-by a:hover .bonsai-powered-by-dot {\n width: 0.5rem;\n transform: scale(1);\n}\n\n/* ============================================================================\n * RESULTS SECTION\n * ============================================================================ */\n.bonsai-results {\n margin-top: var(--bonsai-space-2);\n padding: var(--bonsai-space-4);\n opacity: 0;\n transform: translateY(20px);\n transition:\n opacity var(--bonsai-duration-slow) var(--bonsai-easing),\n transform var(--bonsai-duration-slow) var(--bonsai-easing);\n}\n\n.bonsai-results.bonsai-visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* AI Summary / Recommendation Text */\n.bonsai-summary {\n margin-bottom: var(--bonsai-space-6);\n}\n\n.bonsai-summary-text {\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-base);\n line-height: 1.6;\n color: var(--bonsai-results-text-color, var(--bonsai-text-color));\n margin: 0;\n}\n\n/* Markdown formatting within summary text */\n.bonsai-summary-text h1,\n.bonsai-summary-text h2,\n.bonsai-summary-text h3,\n.bonsai-summary-text h4,\n.bonsai-summary-text h5,\n.bonsai-summary-text h6 {\n font-family: var(--bonsai-font-heading);\n font-weight: 600;\n color: var(--bonsai-results-text-color, var(--bonsai-text-color));\n margin: var(--bonsai-space-4) 0 var(--bonsai-space-2) 0;\n line-height: 1.25;\n}\n\n.bonsai-summary-text h1 {\n font-size: var(--bonsai-font-size-xl);\n}\n\n.bonsai-summary-text h2 {\n font-size: var(--bonsai-font-size-lg);\n}\n\n.bonsai-summary-text h3 {\n font-size: var(--bonsai-font-size-base);\n}\n\n.bonsai-summary-text h4,\n.bonsai-summary-text h5,\n.bonsai-summary-text h6 {\n font-size: var(--bonsai-font-size-sm);\n}\n\n.bonsai-summary-text p {\n margin: var(--bonsai-space-2) 0;\n}\n\n.bonsai-summary-text p:first-child {\n margin-top: 0;\n}\n\n.bonsai-summary-text p:last-child {\n margin-bottom: 0;\n}\n\n.bonsai-summary-text strong {\n font-weight: 600;\n color: var(--bonsai-results-text-color, var(--bonsai-text-color));\n}\n\n.bonsai-summary-text em {\n font-style: italic;\n}\n\n.bonsai-summary-text ul,\n.bonsai-summary-text ol {\n margin: var(--bonsai-space-2) 0;\n padding-left: var(--bonsai-space-5);\n}\n\n.bonsai-summary-text li {\n margin: var(--bonsai-space-1) 0;\n}\n\n.bonsai-summary-text a {\n color: var(--bonsai-brand-color);\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n.bonsai-summary-text a:hover {\n text-decoration: none;\n}\n\n/* Results Header */\n.bonsai-results-header {\n font-family: var(--bonsai-font-heading);\n font-size: var(--bonsai-font-size-sm);\n font-weight: 500;\n color: var(--bonsai-muted-color);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0 0 var(--bonsai-space-3) 0;\n /* border-top: 1px solid rgba(0, 0, 0, 0.06); */\n}\n\n/* Results Grid */\n.bonsai-results-grid {\n display: grid;\n grid-template-columns: repeat(var(--bonsai-results-columns, 3), 1fr);\n gap: var(--bonsai-space-4);\n}\n\n@media (max-width: 640px) {\n .bonsai-results-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 400px) {\n .bonsai-results-grid {\n grid-template-columns: 1fr;\n }\n}\n\n/* ============================================================================\n * RESULT CARD\n * ============================================================================ */\n.bonsai-result-card {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--bonsai-radius-md);\n border: 1px solid var(--bonsai-border-color);\n background-color: var(--bonsai-card-bg);\n text-decoration: none;\n cursor: pointer;\n opacity: 0;\n transform: translateY(10px);\n transition:\n opacity var(--bonsai-duration-base) var(--bonsai-easing),\n transform var(--bonsai-duration-base) var(--bonsai-easing),\n border-color var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-result-card.bonsai-animate-in {\n opacity: 1;\n transform: translateY(0);\n}\n\n.bonsai-result-card:hover {\n border-color: var(--bonsai-border-color-hover);\n}\n\n/* Card Image */\n.bonsai-result-image {\n width: 100%;\n aspect-ratio: 1;\n object-fit: var(--bonsai-image-object-fit, cover);\n background-color: var(--bonsai-surface-color);\n}\n\n.bonsai-result-image-placeholder {\n width: 100%;\n aspect-ratio: 1;\n background-color: var(--bonsai-surface-color);\n opacity: 0.2;\n}\n\n/* Card Content */\n.bonsai-result-content {\n flex: 1;\n min-width: 0;\n padding: var(--bonsai-space-3) var(--bonsai-space-4);\n transition: background-color var(--bonsai-duration-fast) var(--bonsai-easing);\n}\n\n.bonsai-result-card:hover .bonsai-result-content {\n background-color: var(--bonsai-hover-bg);\n}\n\n.bonsai-result-title {\n font-family: var(--bonsai-font-heading);\n font-size: var(--bonsai-font-size-base);\n font-weight: 500;\n line-height: 1.25;\n color: var(--bonsai-card-text-color, var(--bonsai-text-color));\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.bonsai-result-price-container {\n display: flex;\n align-items: center;\n gap: var(--bonsai-space-2);\n margin: var(--bonsai-space-1) 0 0 0;\n}\n\n.bonsai-result-caption {\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n line-height: 1.5;\n color: var(--bonsai-card-text-color, var(--bonsai-text-color));\n opacity: 0.75;\n margin: var(--bonsai-space-1) 0 0 0;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.bonsai-result-price {\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n font-weight: 600;\n line-height: 1.5;\n color: var(--bonsai-card-text-color, var(--bonsai-text-color));\n}\n\n.bonsai-result-compare-at {\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n line-height: 1.5;\n color: var(--bonsai-card-text-color, var(--bonsai-text-color));\n opacity: 0.5;\n margin: 0;\n text-decoration: line-through;\n}\n\n/* ============================================================================\n * RECOMMENDATIONS LOADING INDICATOR\n * ============================================================================ */\n.bonsai-recommendations-loading {\n display: flex;\n align-items: center;\n gap: var(--bonsai-space-3);\n padding: var(--bonsai-space-4);\n margin-bottom: var(--bonsai-space-4);\n background-color: var(--bonsai-input-bg);\n border-radius: var(--bonsai-radius-lg);\n}\n\n.bonsai-recommendations-loading-spinner {\n width: var(--bonsai-icon-size);\n height: var(--bonsai-icon-size);\n animation: bonsai-spin 1s linear infinite;\n}\n\n.bonsai-recommendations-loading-spinner svg {\n width: 100%;\n height: 100%;\n}\n\n.bonsai-recommendations-loading-text {\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-muted-color);\n}\n\n/* ============================================================================\n * RECOMMENDATION PRODUCTS (existing SDK feature)\n * ============================================================================ */\n.bonsai-recommendation-products {\n margin-top: var(--bonsai-space-4);\n padding-top: var(--bonsai-space-4);\n}\n\n.bonsai-recommendation-products-header {\n font-family: var(--bonsai-font-heading);\n font-size: var(--bonsai-font-size-xl);\n font-weight: 500;\n color: var(--bonsai-results-text-color, var(--bonsai-text-color));\n margin: 0 0 var(--bonsai-space-3) 0;\n}\n\n/* ============================================================================\n * EMPTY & ERROR STATES\n * ============================================================================ */\n.bonsai-empty-state {\n text-align: center;\n padding: var(--bonsai-space-6);\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n color: var(--bonsai-muted-color);\n}\n\n.bonsai-error {\n padding: var(--bonsai-space-4);\n border-radius: var(--bonsai-radius-lg);\n background-color: var(--bonsai-error-bg);\n color: var(--bonsai-error-color);\n font-family: var(--bonsai-font-body);\n font-size: var(--bonsai-font-size-sm);\n}\n\n/* ============================================================================\n * UTILITY CLASSES\n * ============================================================================ */\n.bonsai-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n";
5
+
6
+ //#endregion
7
+ //#region src/bonsai-search-webcomponent.ts
8
+ const SDK_CSS$1 = sdk_default || "";
9
+ if (typeof window !== "undefined" && !SDK_CSS$1) console.warn("BonsaiSearch: CSS not loaded from file, will use fallback");
10
+ /**
11
+ * Web Component for Bonsai Search
12
+ * Fully encapsulated with Shadow DOM, no inherited styles
13
+ *
14
+ * Usage:
15
+ * <bonsai-search
16
+ * api-key="your-api-key"
17
+ * base-url="https://api.hibonsai.com/rest/search/v2/"
18
+ * placeholder="Describe what you're looking for..."
19
+ * suggestions='["suggestion 1", "suggestion 2"]'
20
+ * max-results="20"
21
+ * render-price
22
+ * brand-color="#0A5B3B"
23
+ * text-color="#303030"
24
+ * muted-color="#9CA3AF"
25
+ * input-bg="#f5f5f5"
26
+ * card-bg="#f5f5f5"
27
+ * image-object-fit="cover"
28
+ * featured-items-label="Featured Items"
29
+ * more-items-label="More Items"
30
+ * items-label="Items">
31
+ * </bonsai-search>
32
+ */
33
+ var BonsaiSearchElement = class extends HTMLElement {
34
+ search = null;
35
+ shadow;
36
+ container = null;
37
+ static get observedAttributes() {
38
+ return [
39
+ "api-key",
40
+ "base-url",
41
+ "placeholder",
42
+ "suggestions",
43
+ "max-results",
44
+ "timeout-ms",
45
+ "render-price",
46
+ "render-price-with-title",
47
+ "brand-color",
48
+ "text-color",
49
+ "suggestions-text-color",
50
+ "input-text-color",
51
+ "results-text-color",
52
+ "card-text-color",
53
+ "results-columns",
54
+ "suggestions-hover-bg",
55
+ "muted-color",
56
+ "input-bg",
57
+ "card-bg",
58
+ "surface-color",
59
+ "canvas-color",
60
+ "image-object-fit",
61
+ "markdown",
62
+ "theme",
63
+ "featured-items-label",
64
+ "more-items-label",
65
+ "items-label"
66
+ ];
67
+ }
68
+ constructor() {
69
+ super();
70
+ this.shadow = this.attachShadow({ mode: "closed" });
71
+ }
72
+ connectedCallback() {
73
+ this.render();
74
+ this.initialize();
75
+ }
76
+ disconnectedCallback() {
77
+ this.search?.destroy();
78
+ this.search = null;
79
+ }
80
+ attributeChangedCallback(name, oldValue, newValue) {
81
+ if (oldValue === newValue || !this.search) return;
82
+ if ([
83
+ "api-key",
84
+ "base-url",
85
+ "placeholder",
86
+ "suggestions",
87
+ "markdown",
88
+ "featured-items-label",
89
+ "more-items-label",
90
+ "items-label"
91
+ ].includes(name)) {
92
+ this.disconnectedCallback();
93
+ this.connectedCallback();
94
+ } else this.updateStyles();
95
+ }
96
+ render() {
97
+ let css = SDK_CSS$1 || "";
98
+ if (!css || css.trim().length < 100) {
99
+ console.warn("BonsaiSearch: CSS not loaded, using fallback");
100
+ css = this.getDefaultCSS();
101
+ }
102
+ css = css.replace(/:root\s*\{/g, ":host {");
103
+ const style = document.createElement("style");
104
+ style.textContent = css;
105
+ this.shadow.appendChild(style);
106
+ this.container = document.createElement("div");
107
+ this.container.style.width = "100%";
108
+ this.shadow.appendChild(this.container);
109
+ this.updateStyles();
110
+ }
111
+ updateStyles() {
112
+ const themeAttr = this.getAttribute("theme") || "light";
113
+ const normalizedTheme = themeAttr === "dark" || themeAttr === "auto" ? themeAttr : "light";
114
+ const prefersDark = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
115
+ const resolvedTheme = normalizedTheme === "auto" ? prefersDark ? "dark" : "light" : normalizedTheme;
116
+ this.setAttribute("data-theme", resolvedTheme);
117
+ const imageObjectFitAttr = this.getAttribute("image-object-fit");
118
+ const validObjectFit = imageObjectFitAttr === "contain" ? "contain" : imageObjectFitAttr === "cover" ? "cover" : null;
119
+ const setOrClear = (name, value) => {
120
+ if (value && value.trim().length > 0) this.style.setProperty(name, value);
121
+ else this.style.removeProperty(name);
122
+ };
123
+ setOrClear("--bonsai-brand-color", this.getAttribute("brand-color"));
124
+ setOrClear("--bonsai-text-color", this.getAttribute("text-color"));
125
+ const suggestionsTextColor = this.getAttribute("suggestions-text-color");
126
+ const inputTextColor = this.getAttribute("input-text-color");
127
+ setOrClear("--bonsai-suggestions-text-color", suggestionsTextColor || inputTextColor);
128
+ setOrClear("--bonsai-input-text-color", this.getAttribute("input-text-color"));
129
+ setOrClear("--bonsai-results-text-color", this.getAttribute("results-text-color"));
130
+ setOrClear("--bonsai-card-text-color", this.getAttribute("card-text-color"));
131
+ setOrClear("--bonsai-results-columns", this.getAttribute("results-columns"));
132
+ setOrClear("--bonsai-suggestions-hover-bg", this.getAttribute("suggestions-hover-bg"));
133
+ setOrClear("--bonsai-muted-color", this.getAttribute("muted-color"));
134
+ setOrClear("--bonsai-input-bg", this.getAttribute("input-bg"));
135
+ setOrClear("--bonsai-card-bg", this.getAttribute("card-bg"));
136
+ setOrClear("--bonsai-surface-color", this.getAttribute("surface-color"));
137
+ setOrClear("--bonsai-canvas-color", this.getAttribute("canvas-color"));
138
+ setOrClear("--bonsai-image-object-fit", validObjectFit);
139
+ }
140
+ initialize() {
141
+ if (!this.container) return;
142
+ const apiKey = this.getAttribute("api-key");
143
+ if (!apiKey) {
144
+ console.error("bonsai-search: api-key attribute is required");
145
+ return;
146
+ }
147
+ const suggestionsAttr = this.getAttribute("suggestions");
148
+ let suggestions = [];
149
+ if (suggestionsAttr) try {
150
+ suggestions = JSON.parse(suggestionsAttr);
151
+ } catch (e) {
152
+ console.warn("bonsai-search: Invalid suggestions JSON, using empty array");
153
+ }
154
+ const imageObjectFit = this.getAttribute("image-object-fit") === "contain" ? "contain" : "cover";
155
+ this.search = new BonsaiSearch({
156
+ container: this.container,
157
+ apiKey,
158
+ baseUrl: this.getAttribute("base-url") || void 0,
159
+ placeholder: this.getAttribute("placeholder") || "Describe what you're looking for...",
160
+ suggestions,
161
+ debounceMs: 0,
162
+ maxResults: this.getAttribute("max-results") ? parseInt(this.getAttribute("max-results")) : 50,
163
+ timeoutMs: this.getAttribute("timeout-ms") ? parseInt(this.getAttribute("timeout-ms")) : 3e4,
164
+ renderPrice: this.hasAttribute("render-price"),
165
+ renderPriceWithTitle: this.getAttribute("render-price-with-title") !== "false",
166
+ imageObjectFit,
167
+ markdown: this.hasAttribute("markdown"),
168
+ featuredItemsLabel: this.getAttribute("featured-items-label") || void 0,
169
+ moreItemsLabel: this.getAttribute("more-items-label") || void 0,
170
+ itemsLabel: this.getAttribute("items-label") || void 0,
171
+ onPrice: (result, price) => {
172
+ return price || "";
173
+ },
174
+ onSearch: (query) => {
175
+ this.dispatchEvent(new CustomEvent("search", {
176
+ detail: { query },
177
+ bubbles: true,
178
+ composed: true
179
+ }));
180
+ },
181
+ onResults: (results) => {
182
+ this.dispatchEvent(new CustomEvent("results", {
183
+ detail: { results },
184
+ bubbles: true,
185
+ composed: true
186
+ }));
187
+ },
188
+ onAi: (data) => {
189
+ this.dispatchEvent(new CustomEvent("ai", {
190
+ detail: data,
191
+ bubbles: true,
192
+ composed: true
193
+ }));
194
+ },
195
+ onError: (error) => {
196
+ this.dispatchEvent(new CustomEvent("error", {
197
+ detail: { error: error.message },
198
+ bubbles: true,
199
+ composed: true
200
+ }));
201
+ }
202
+ });
203
+ }
204
+ setSuggestion(suggestion) {
205
+ this.search?.setSuggestion(suggestion);
206
+ }
207
+ clear() {
208
+ this.search?.clear();
209
+ }
210
+ focus() {
211
+ this.search?.focus();
212
+ }
213
+ getDefaultCSS() {
214
+ return `
215
+ :host {
216
+ display: block;
217
+ width: 100%;
218
+ --bonsai-brand-color: #0A5B3B;
219
+ --bonsai-text-color: #303030;
220
+ --bonsai-suggestions-text-color: #303030;
221
+ --bonsai-input-text-color: #303030;
222
+ --bonsai-results-text-color: #303030;
223
+ --bonsai-card-text-color: #303030;
224
+ --bonsai-suggestions-hover-bg: rgba(0, 0, 0, 0.04);
225
+ --bonsai-muted-color: #9CA3AF;
226
+ --bonsai-input-bg: #f5f5f5;
227
+ --bonsai-card-bg: #f5f5f5;
228
+ --bonsai-surface-color: #ffffff;
229
+ --bonsai-canvas-color: #fafafa;
230
+ --bonsai-image-object-fit: cover;
231
+ --bonsai-border-color: rgba(0, 0, 0, 0.06);
232
+ --bonsai-border-color-hover: rgba(0, 0, 0, 0.12);
233
+ --bonsai-hover-bg: rgba(0, 0, 0, 0.04);
234
+ --bonsai-error-bg: rgba(220, 53, 69, 0.1);
235
+ --bonsai-error-color: #c82333;
236
+ }
237
+
238
+ :host([data-theme="dark"]) {
239
+ --bonsai-text-color: #e5e7eb;
240
+ --bonsai-suggestions-text-color: #e5e7eb;
241
+ --bonsai-input-text-color: #e5e7eb;
242
+ --bonsai-results-text-color: #e5e7eb;
243
+ --bonsai-card-text-color: #e5e7eb;
244
+ --bonsai-suggestions-hover-bg: rgba(255, 255, 255, 0.06);
245
+ --bonsai-muted-color: #a1a1aa;
246
+ --bonsai-input-bg: #1f2229;
247
+ --bonsai-card-bg: #1f2229;
248
+ --bonsai-surface-color: #181a20;
249
+ --bonsai-canvas-color: #0f1115;
250
+ --bonsai-border-color: rgba(255, 255, 255, 0.08);
251
+ --bonsai-border-color-hover: rgba(255, 255, 255, 0.14);
252
+ --bonsai-hover-bg: rgba(255, 255, 255, 0.06);
253
+ --bonsai-error-bg: rgba(248, 113, 113, 0.15);
254
+ --bonsai-error-color: #fca5a5;
255
+ }
256
+ `;
257
+ }
258
+ };
259
+ if (typeof window !== "undefined" && !customElements.get("bonsai-search")) customElements.define("bonsai-search", BonsaiSearchElement);
260
+
261
+ //#endregion
262
+ //#region src/searchbar.ts
263
+ const VERSION$1 = "3.0.8";
264
+ const ICONS$1 = {
265
+ search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
266
+ <circle cx="11" cy="11" r="8"></circle>
267
+ <path d="m21 21-4.35-4.35"></path>
268
+ </svg>`,
269
+ arrowRight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
270
+ <path d="M5 12h14"></path>
271
+ <path d="m12 5 7 7-7 7"></path>
272
+ </svg>`,
273
+ close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
274
+ <path d="M18 6L6 18"></path>
275
+ <path d="M6 6l12 12"></path>
276
+ </svg>`
277
+ };
278
+ const SEARCHBAR_STYLES = `
279
+ @keyframes bonsai-searchbar-fade-in {
280
+ from { opacity: 0; transform: translateY(-10px); }
281
+ to { opacity: 1; transform: translateY(0); }
282
+ }
283
+ @keyframes bonsai-searchbar-scale-in {
284
+ from { opacity: 0; transform: scale(0.8); }
285
+ to { opacity: 1; transform: scale(1); }
286
+ }
287
+ .bonsai-searchbar-container {
288
+ margin: 24px auto;
289
+ font-family: system-ui, -apple-system, sans-serif;
290
+ }
291
+ .bonsai-searchbar-wrapper {
292
+ position: relative;
293
+ }
294
+ .bonsai-searchbar {
295
+ display: flex;
296
+ align-items: center;
297
+ min-height: 56px;
298
+ position: relative;
299
+ z-index: 2;
300
+ background-color: var(--bonsai-input-bg);
301
+ border-radius: 8px;
302
+ transition: box-shadow 200ms ease-out;
303
+ }
304
+ .bonsai-searchbar.bonsai-searchbar-focused {
305
+ box-shadow: 0 0 0 2px var(--bonsai-brand-color);
306
+ }
307
+ .bonsai-searchbar-icon {
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ padding-left: 16px;
312
+ padding-right: 8px;
313
+ color: var(--bonsai-muted-color);
314
+ transition: color 200ms ease-out;
315
+ }
316
+ .bonsai-searchbar-icon svg {
317
+ width: 20px;
318
+ height: 20px;
319
+ }
320
+ .bonsai-searchbar:hover .bonsai-searchbar-icon,
321
+ .bonsai-searchbar.bonsai-searchbar-focused .bonsai-searchbar-icon {
322
+ color: var(--bonsai-input-text-color, var(--bonsai-text-color));
323
+ }
324
+ .bonsai-searchbar-input {
325
+ flex: 1;
326
+ min-width: 0;
327
+ padding: 16px 16px 16px 0;
328
+ border: none;
329
+ background: transparent;
330
+ font-family: inherit;
331
+ font-size: 16px;
332
+ color: var(--bonsai-input-text-color, var(--bonsai-text-color));
333
+ outline: none;
334
+ }
335
+ .bonsai-searchbar-input::placeholder {
336
+ color: var(--bonsai-muted-color);
337
+ }
338
+ .bonsai-searchbar-actions {
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 8px;
342
+ padding-right: 12px;
343
+ flex: 0 0 auto;
344
+ min-width: max-content;
345
+ }
346
+ .bonsai-searchbar-submit {
347
+ display: none;
348
+ align-items: center;
349
+ justify-content: center;
350
+ width: 32px;
351
+ height: 32px;
352
+ padding: 8px;
353
+ border: none;
354
+ border-radius: 8px;
355
+ background-color: var(--bonsai-brand-color);
356
+ cursor: pointer;
357
+ touch-action: manipulation;
358
+ flex-shrink: 0;
359
+ color: white;
360
+ animation: bonsai-searchbar-scale-in 0.2s ease-out forwards;
361
+ }
362
+ .bonsai-searchbar-submit.bonsai-searchbar-visible {
363
+ display: flex;
364
+ }
365
+ .bonsai-searchbar-submit svg {
366
+ width: 16px;
367
+ height: 16px;
368
+ }
369
+ .bonsai-searchbar-close {
370
+ display: none;
371
+ align-items: center;
372
+ justify-content: center;
373
+ width: 32px;
374
+ height: 32px;
375
+ padding: 8px;
376
+ border: none;
377
+ border-radius: 8px;
378
+ background: transparent;
379
+ cursor: pointer;
380
+ touch-action: manipulation;
381
+ flex-shrink: 0;
382
+ color: var(--bonsai-muted-color);
383
+ transition: color 200ms ease-out;
384
+ }
385
+ .bonsai-searchbar-close.bonsai-searchbar-visible {
386
+ display: flex;
387
+ }
388
+ .bonsai-searchbar-close:hover {
389
+ color: var(--bonsai-text-color);
390
+ }
391
+ .bonsai-searchbar-close svg {
392
+ width: 16px;
393
+ height: 16px;
394
+ }
395
+ .bonsai-searchbar-suggestions {
396
+ position: absolute;
397
+ top: calc(100% + 4px);
398
+ left: 0;
399
+ right: 0;
400
+ background-color: var(--bonsai-input-bg);
401
+ border-radius: 8px;
402
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
403
+ overflow: hidden;
404
+ z-index: 10;
405
+ display: none;
406
+ animation: bonsai-searchbar-fade-in 0.2s ease-out forwards;
407
+ }
408
+ .bonsai-searchbar-suggestions.bonsai-searchbar-visible {
409
+ display: block;
410
+ }
411
+ .bonsai-searchbar-suggestion {
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: space-between;
415
+ width: 100%;
416
+ padding: 8px 16px;
417
+ border: none;
418
+ font-family: inherit;
419
+ font-size: 14px;
420
+ color: var(--bonsai-suggestions-text-color, var(--bonsai-text-color));
421
+ text-align: left;
422
+ background-color: var(--bonsai-input-bg);
423
+ cursor: pointer;
424
+ transition: background-color 150ms ease-out;
425
+ }
426
+ .bonsai-searchbar-suggestion:hover {
427
+ background-color: var(--bonsai-suggestions-hover-bg, rgba(0, 0, 0, 0.04));
428
+ }
429
+ .bonsai-searchbar-suggestion-arrow {
430
+ color: var(--bonsai-muted-color);
431
+ opacity: 0;
432
+ visibility: hidden;
433
+ transition: opacity 150ms ease-out, visibility 0s linear 150ms;
434
+ }
435
+ .bonsai-searchbar-suggestion:hover .bonsai-searchbar-suggestion-arrow {
436
+ opacity: 1;
437
+ visibility: visible;
438
+ transition-delay: 0s;
439
+ }
440
+ .bonsai-searchbar-suggestion-arrow svg {
441
+ width: 16px;
442
+ height: 16px;
443
+ }
444
+ .bonsai-searchbar-powered {
445
+ position: relative;
446
+ z-index: 1;
447
+ height: 24px;
448
+ margin-top: 4px;
449
+ text-align: right;
450
+ font-family: ui-monospace, monospace;
451
+ font-size: 12px;
452
+ color: var(--bonsai-muted-color);
453
+ }
454
+ .bonsai-searchbar-powered-link {
455
+ color: var(--bonsai-brand-color);
456
+ text-decoration: none;
457
+ display: inline-flex;
458
+ align-items: center;
459
+ gap: 0;
460
+ transition: gap 0.2s ease-out;
461
+ }
462
+ .bonsai-searchbar-powered-link:hover {
463
+ gap: 4px;
464
+ }
465
+ .bonsai-searchbar-powered-dot {
466
+ display: inline-block;
467
+ height: 8px;
468
+ width: 0;
469
+ border-radius: 50%;
470
+ background-color: var(--bonsai-brand-color);
471
+ transform: scale(0);
472
+ transition: transform 0.2s ease-out, width 0.2s ease-out;
473
+ }
474
+ .bonsai-searchbar-powered-link:hover .bonsai-searchbar-powered-dot {
475
+ transform: scale(1);
476
+ width: 8px;
477
+ }
478
+ `;
479
+ var BonsaiSearchBar = class {
480
+ config;
481
+ state = "default";
482
+ container;
483
+ searchBar = null;
484
+ searchInput = null;
485
+ submitBtn = null;
486
+ suggestionsDropdown = null;
487
+ query = "";
488
+ stylesInjected = false;
489
+ constructor(config) {
490
+ this.container = config.container;
491
+ const inputBg = config.inputBg || config.inputBackground || "#f5f5f5";
492
+ this.config = {
493
+ container: config.container,
494
+ searchPath: config.searchPath || "/ai-search",
495
+ placeholder: config.placeholder || "Describe what you're looking for...",
496
+ suggestions: config.suggestions || [],
497
+ brandColor: config.brandColor || "#0A5B3B",
498
+ textColor: config.textColor || "#303030",
499
+ suggestionsTextColor: config.suggestionsTextColor || "",
500
+ inputTextColor: config.inputTextColor || "",
501
+ mutedColor: config.mutedColor || "#9CA3AF",
502
+ inputBg,
503
+ inputBackground: inputBg,
504
+ inputOpacity: config.inputOpacity || "1",
505
+ suggestionsHoverBg: config.suggestionsHoverBg || "",
506
+ theme: config.theme || "light",
507
+ closeButton: config.closeButton || false,
508
+ onClose: config.onClose || (() => {}),
509
+ classAdditions: config.classAdditions || {}
510
+ };
511
+ this.validateConfig();
512
+ this.injectStyles();
513
+ this.render();
514
+ this.attachEventListeners();
515
+ }
516
+ /** Get the SDK version */
517
+ static get version() {
518
+ return VERSION$1;
519
+ }
520
+ /** Validate configuration */
521
+ validateConfig() {
522
+ if (!this.config.container) throw new Error("BonsaiSearchBar: container element is required");
523
+ }
524
+ /** Build class name string with any configured additional classes */
525
+ buildClassName(baseClass) {
526
+ const additions = this.config.classAdditions[baseClass];
527
+ if (additions && additions.length > 0) return `${baseClass} ${additions.join(" ")}`;
528
+ return baseClass;
529
+ }
530
+ /** Apply a shadow part name for external styling */
531
+ applyPart(el, part) {
532
+ el.setAttribute("part", part);
533
+ }
534
+ /** Inject CSS styles into the document */
535
+ injectStyles() {
536
+ if (this.stylesInjected) return;
537
+ if (document.getElementById("bonsai-searchbar-styles")) {
538
+ this.stylesInjected = true;
539
+ return;
540
+ }
541
+ const styleElement = document.createElement("style");
542
+ styleElement.id = "bonsai-searchbar-styles";
543
+ styleElement.textContent = SEARCHBAR_STYLES;
544
+ document.head.appendChild(styleElement);
545
+ this.stylesInjected = true;
546
+ }
547
+ /** Render the search bar UI */
548
+ render() {
549
+ this.container.innerHTML = "";
550
+ this.container.classList.add("bonsai-searchbar-container");
551
+ this.applyPart(this.container, "searchbar-container");
552
+ this.container.setAttribute("data-theme", this.config.theme);
553
+ this.container.style.setProperty("--bonsai-brand-color", this.config.brandColor);
554
+ this.container.style.setProperty("--bonsai-text-color", this.config.textColor);
555
+ this.container.style.setProperty("--bonsai-muted-color", this.config.mutedColor);
556
+ this.container.style.setProperty("--bonsai-input-bg", this.config.inputBg);
557
+ if (this.config.suggestionsTextColor) this.container.style.setProperty("--bonsai-suggestions-text-color", this.config.suggestionsTextColor);
558
+ if (this.config.inputTextColor) this.container.style.setProperty("--bonsai-input-text-color", this.config.inputTextColor);
559
+ if (this.config.suggestionsHoverBg) this.container.style.setProperty("--bonsai-suggestions-hover-bg", this.config.suggestionsHoverBg);
560
+ const wrapper = document.createElement("div");
561
+ wrapper.className = this.buildClassName("bonsai-searchbar-wrapper");
562
+ this.applyPart(wrapper, "searchbar-wrapper");
563
+ this.searchBar = document.createElement("div");
564
+ this.searchBar.className = this.buildClassName("bonsai-searchbar");
565
+ this.applyPart(this.searchBar, "searchbar");
566
+ this.searchBar.style.opacity = this.config.inputOpacity;
567
+ const searchIcon = document.createElement("div");
568
+ searchIcon.className = this.buildClassName("bonsai-searchbar-icon");
569
+ this.applyPart(searchIcon, "searchbar-icon");
570
+ searchIcon.innerHTML = ICONS$1.search;
571
+ this.searchBar.appendChild(searchIcon);
572
+ this.searchInput = document.createElement("input");
573
+ this.searchInput.type = "text";
574
+ this.searchInput.className = this.buildClassName("bonsai-searchbar-input");
575
+ this.applyPart(this.searchInput, "searchbar-input");
576
+ this.searchInput.placeholder = this.config.placeholder;
577
+ this.searchInput.setAttribute("aria-label", "Search input");
578
+ this.searchBar.appendChild(this.searchInput);
579
+ const actionsContainer = document.createElement("div");
580
+ actionsContainer.className = this.buildClassName("bonsai-searchbar-actions");
581
+ this.applyPart(actionsContainer, "searchbar-actions");
582
+ this.submitBtn = document.createElement("button");
583
+ this.submitBtn.type = "button";
584
+ this.submitBtn.className = this.buildClassName("bonsai-searchbar-submit");
585
+ this.applyPart(this.submitBtn, "searchbar-submit");
586
+ this.submitBtn.setAttribute("aria-label", "Submit search");
587
+ this.submitBtn.innerHTML = ICONS$1.arrowRight;
588
+ actionsContainer.appendChild(this.submitBtn);
589
+ const closeBtn = document.createElement("button");
590
+ closeBtn.type = "button";
591
+ closeBtn.className = this.buildClassName("bonsai-searchbar-close");
592
+ if (this.config.closeButton) closeBtn.classList.add("bonsai-searchbar-visible");
593
+ this.applyPart(closeBtn, "searchbar-close");
594
+ closeBtn.setAttribute("aria-label", "Close search");
595
+ closeBtn.innerHTML = ICONS$1.close;
596
+ closeBtn.addEventListener("click", () => this.config.onClose());
597
+ actionsContainer.appendChild(closeBtn);
598
+ this.searchBar.appendChild(actionsContainer);
599
+ wrapper.appendChild(this.searchBar);
600
+ if (this.config.suggestions.length > 0) {
601
+ this.suggestionsDropdown = document.createElement("div");
602
+ this.suggestionsDropdown.className = this.buildClassName("bonsai-searchbar-suggestions");
603
+ this.applyPart(this.suggestionsDropdown, "searchbar-suggestions");
604
+ this.suggestionsDropdown.setAttribute("role", "listbox");
605
+ this.renderSuggestions();
606
+ wrapper.appendChild(this.suggestionsDropdown);
607
+ }
608
+ this.container.appendChild(wrapper);
609
+ const poweredBy = document.createElement("div");
610
+ poweredBy.className = this.buildClassName("bonsai-searchbar-powered");
611
+ poweredBy.innerHTML = `powered by <a href="https://www.hibonsai.com/" target="_blank" rel="noopener" class="bonsai-searchbar-powered-link v${VERSION$1}">Bonsai<span class="bonsai-searchbar-powered-dot"></span></a>`;
612
+ this.applyPart(poweredBy, "searchbar-powered");
613
+ const poweredLink = poweredBy.querySelector(".bonsai-searchbar-powered-link");
614
+ if (poweredLink) this.applyPart(poweredLink, "searchbar-powered-link");
615
+ const poweredDot = poweredBy.querySelector(".bonsai-searchbar-powered-dot");
616
+ if (poweredDot) this.applyPart(poweredDot, "searchbar-powered-dot");
617
+ this.container.appendChild(poweredBy);
618
+ }
619
+ /** Render suggestions in the dropdown */
620
+ renderSuggestions() {
621
+ if (!this.suggestionsDropdown) return;
622
+ this.suggestionsDropdown.innerHTML = "";
623
+ (this.query ? this.config.suggestions.filter((s) => s.toLowerCase().includes(this.query.toLowerCase())).slice(0, 5) : this.config.suggestions.slice(0, 5)).forEach((suggestion) => {
624
+ const item = document.createElement("button");
625
+ item.type = "button";
626
+ item.className = this.buildClassName("bonsai-searchbar-suggestion");
627
+ this.applyPart(item, "searchbar-suggestion");
628
+ item.setAttribute("role", "option");
629
+ const text = document.createElement("span");
630
+ text.textContent = suggestion;
631
+ this.applyPart(text, "searchbar-suggestion-text");
632
+ item.appendChild(text);
633
+ const arrow = document.createElement("div");
634
+ arrow.className = this.buildClassName("bonsai-searchbar-suggestion-arrow");
635
+ this.applyPart(arrow, "searchbar-suggestion-arrow");
636
+ arrow.innerHTML = ICONS$1.arrowRight;
637
+ item.appendChild(arrow);
638
+ item.addEventListener("click", () => this.handleSuggestionClick(suggestion));
639
+ this.suggestionsDropdown.appendChild(item);
640
+ });
641
+ }
642
+ /** Attach event listeners */
643
+ attachEventListeners() {
644
+ if (!this.searchInput) return;
645
+ this.searchInput.addEventListener("focus", () => this.handleFocus());
646
+ this.searchInput.addEventListener("blur", () => this.handleBlur());
647
+ this.searchInput.addEventListener("input", (e) => this.handleInput(e));
648
+ this.searchInput.addEventListener("keydown", (e) => this.handleKeyDown(e));
649
+ this.submitBtn?.addEventListener("click", () => this.handleSubmit());
650
+ }
651
+ /** Handle input focus */
652
+ handleFocus() {
653
+ if (this.state === "default") this.setState("focused");
654
+ this.searchBar?.classList.add("bonsai-searchbar-focused");
655
+ if (this.suggestionsDropdown) {
656
+ this.renderSuggestions();
657
+ this.suggestionsDropdown.classList.add("bonsai-searchbar-visible");
658
+ }
659
+ }
660
+ /** Handle input blur */
661
+ handleBlur() {
662
+ this.searchBar?.classList.remove("bonsai-searchbar-focused");
663
+ setTimeout(() => {
664
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
665
+ }, 200);
666
+ }
667
+ /** Handle input change */
668
+ handleInput(e) {
669
+ this.query = e.target.value;
670
+ if (this.query) {
671
+ this.setState("filled");
672
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
673
+ } else {
674
+ this.setState("focused");
675
+ if (this.suggestionsDropdown) {
676
+ this.renderSuggestions();
677
+ this.suggestionsDropdown.classList.add("bonsai-searchbar-visible");
678
+ }
679
+ }
680
+ }
681
+ /** Handle key down */
682
+ handleKeyDown(e) {
683
+ if (e.key === "Enter" && this.query) {
684
+ e.preventDefault();
685
+ this.handleSubmit();
686
+ }
687
+ }
688
+ /** Handle suggestion click */
689
+ handleSuggestionClick(suggestion) {
690
+ this.query = suggestion;
691
+ if (this.searchInput) this.searchInput.value = suggestion;
692
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
693
+ this.handleSubmit();
694
+ }
695
+ /** Handle search submit - redirect to search page */
696
+ handleSubmit(searchQuery) {
697
+ const queryToSearch = searchQuery ?? this.query;
698
+ if (!queryToSearch) return;
699
+ try {
700
+ localStorage.setItem("bonsai-search-query", queryToSearch);
701
+ } catch {}
702
+ const url = `${this.config.searchPath}?q=${encodeURIComponent(queryToSearch)}`;
703
+ window.location.href = url;
704
+ }
705
+ /** Set the current state and update UI */
706
+ setState(newState) {
707
+ this.state = newState;
708
+ this.updateSubmitButton();
709
+ }
710
+ /** Update submit button visibility based on state */
711
+ updateSubmitButton() {
712
+ if (!this.submitBtn) return;
713
+ if (this.state === "filled") this.submitBtn.classList.add("bonsai-searchbar-visible");
714
+ else this.submitBtn.classList.remove("bonsai-searchbar-visible");
715
+ }
716
+ /** Set the search query programmatically */
717
+ setQuery(query) {
718
+ this.query = query;
719
+ if (this.searchInput) this.searchInput.value = query;
720
+ if (query) this.setState("filled");
721
+ else this.setState("default");
722
+ }
723
+ /** Submit the search programmatically */
724
+ submit() {
725
+ this.handleSubmit();
726
+ }
727
+ /** Clear the search input */
728
+ clear() {
729
+ this.query = "";
730
+ if (this.searchInput) this.searchInput.value = "";
731
+ this.setState("default");
732
+ }
733
+ /** Destroy the instance and clean up */
734
+ destroy() {
735
+ this.container.innerHTML = "";
736
+ }
737
+ };
738
+ if (typeof window !== "undefined") window.BonsaiSearchBar = BonsaiSearchBar;
739
+
740
+ //#endregion
741
+ //#region src/bonsai-searchbar-webcomponent.ts
742
+ const SDK_CSS = sdk_default || "";
743
+ if (typeof window !== "undefined" && !SDK_CSS) console.warn("BonsaiSearchBar: CSS not loaded from file, will use fallback");
744
+ /**
745
+ * Web Component for Bonsai Search Bar
746
+ * Fully encapsulated with Shadow DOM, no inherited styles
747
+ *
748
+ * Usage:
749
+ * <bonsai-searchbar
750
+ * search-path="/ai-search"
751
+ * placeholder="Describe what you're looking for..."
752
+ * suggestions='["suggestion 1", "suggestion 2"]'
753
+ * brand-color="#0A5B3B"
754
+ * text-color="#303030"
755
+ * muted-color="#9CA3AF"
756
+ * input-background="#f5f5f5"
757
+ * input-opacity="1"
758
+ * close-button>
759
+ * </bonsai-searchbar>
760
+ */
761
+ var BonsaiSearchBarElement = class extends HTMLElement {
762
+ searchBar = null;
763
+ shadow;
764
+ container = null;
765
+ static get observedAttributes() {
766
+ return [
767
+ "search-path",
768
+ "placeholder",
769
+ "suggestions",
770
+ "brand-color",
771
+ "text-color",
772
+ "suggestions-text-color",
773
+ "input-text-color",
774
+ "muted-color",
775
+ "input-bg",
776
+ "input-background",
777
+ "input-opacity",
778
+ "suggestions-hover-bg",
779
+ "theme",
780
+ "close-button"
781
+ ];
782
+ }
783
+ constructor() {
784
+ super();
785
+ this.shadow = this.attachShadow({ mode: "closed" });
786
+ }
787
+ connectedCallback() {
788
+ this.render();
789
+ this.initialize();
790
+ }
791
+ disconnectedCallback() {
792
+ this.searchBar?.destroy();
793
+ this.searchBar = null;
794
+ }
795
+ attributeChangedCallback(name, oldValue, newValue) {
796
+ if (oldValue === newValue || !this.searchBar) return;
797
+ if ([
798
+ "search-path",
799
+ "placeholder",
800
+ "suggestions"
801
+ ].includes(name)) {
802
+ this.disconnectedCallback();
803
+ this.connectedCallback();
804
+ } else this.updateStyles();
805
+ }
806
+ render() {
807
+ let css = (SDK_CSS + "\n" + SEARCHBAR_STYLES).trim();
808
+ if (!css || css.trim().length < 100) {
809
+ console.warn("BonsaiSearchBar: CSS not loaded, using fallback");
810
+ css = this.getDefaultCSS();
811
+ }
812
+ css = css.replace(/:root\s*\{/g, ":host {");
813
+ const style = document.createElement("style");
814
+ style.textContent = css;
815
+ this.shadow.appendChild(style);
816
+ this.container = document.createElement("div");
817
+ this.container.style.width = "100%";
818
+ this.shadow.appendChild(this.container);
819
+ this.updateStyles();
820
+ }
821
+ updateStyles() {
822
+ const themeAttr = this.getAttribute("theme") || "light";
823
+ const normalizedTheme = themeAttr === "dark" || themeAttr === "auto" ? themeAttr : "light";
824
+ const prefersDark = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
825
+ const resolvedTheme = normalizedTheme === "auto" ? prefersDark ? "dark" : "light" : normalizedTheme;
826
+ this.setAttribute("data-theme", resolvedTheme);
827
+ const setOrClear = (name, value) => {
828
+ if (value && value.trim().length > 0) this.style.setProperty(name, value);
829
+ else this.style.removeProperty(name);
830
+ };
831
+ setOrClear("--bonsai-brand-color", this.getAttribute("brand-color") || "#0A5B3B");
832
+ setOrClear("--bonsai-text-color", this.getAttribute("text-color") || "#303030");
833
+ setOrClear("--bonsai-muted-color", this.getAttribute("muted-color") || "#9CA3AF");
834
+ setOrClear("--bonsai-input-bg", this.getAttribute("input-bg") || this.getAttribute("input-background") || "#f5f5f5");
835
+ const suggestionsTextColor = this.getAttribute("suggestions-text-color");
836
+ const inputTextColor = this.getAttribute("input-text-color");
837
+ setOrClear("--bonsai-suggestions-text-color", suggestionsTextColor || inputTextColor);
838
+ setOrClear("--bonsai-input-text-color", inputTextColor);
839
+ setOrClear("--bonsai-suggestions-hover-bg", this.getAttribute("suggestions-hover-bg"));
840
+ }
841
+ initialize() {
842
+ if (!this.container) return;
843
+ const suggestionsAttr = this.getAttribute("suggestions");
844
+ let suggestions = [];
845
+ if (suggestionsAttr) try {
846
+ suggestions = JSON.parse(suggestionsAttr);
847
+ } catch (e) {
848
+ console.warn("bonsai-searchbar: Invalid suggestions JSON, using empty array");
849
+ }
850
+ const themeAttr = this.getAttribute("theme") || "light";
851
+ const theme = themeAttr === "dark" || themeAttr === "auto" ? themeAttr : "light";
852
+ this.searchBar = new BonsaiSearchBar({
853
+ container: this.container,
854
+ searchPath: this.getAttribute("search-path") || "/ai-search",
855
+ placeholder: this.getAttribute("placeholder") || "Describe what you're looking for...",
856
+ suggestions,
857
+ brandColor: this.getAttribute("brand-color") || "#0A5B3B",
858
+ textColor: this.getAttribute("text-color") || "#303030",
859
+ suggestionsTextColor: this.getAttribute("suggestions-text-color") || void 0,
860
+ inputTextColor: this.getAttribute("input-text-color") || void 0,
861
+ mutedColor: this.getAttribute("muted-color") || "#9CA3AF",
862
+ inputBg: this.getAttribute("input-bg") || this.getAttribute("input-background") || "#f5f5f5",
863
+ inputOpacity: this.getAttribute("input-opacity") || "1",
864
+ suggestionsHoverBg: this.getAttribute("suggestions-hover-bg") || void 0,
865
+ theme,
866
+ closeButton: this.hasAttribute("close-button"),
867
+ onClose: () => {
868
+ this.dispatchEvent(new CustomEvent("close", {
869
+ bubbles: true,
870
+ composed: true
871
+ }));
872
+ }
873
+ });
874
+ }
875
+ setQuery(query) {
876
+ this.searchBar?.setQuery(query);
877
+ }
878
+ submit() {
879
+ this.searchBar?.submit();
880
+ }
881
+ clear() {
882
+ this.searchBar?.clear();
883
+ }
884
+ getDefaultCSS() {
885
+ return `
886
+ :host {
887
+ display: block;
888
+ width: 100%;
889
+ --bonsai-brand-color: #0A5B3B;
890
+ --bonsai-text-color: #303030;
891
+ --bonsai-suggestions-text-color: #303030;
892
+ --bonsai-input-text-color: #303030;
893
+ --bonsai-suggestions-hover-bg: rgba(0, 0, 0, 0.04);
894
+ --bonsai-muted-color: #9CA3AF;
895
+ --bonsai-input-bg: #f5f5f5;
896
+ }
897
+
898
+ :host([data-theme="dark"]) {
899
+ --bonsai-text-color: #e5e7eb;
900
+ --bonsai-suggestions-text-color: #e5e7eb;
901
+ --bonsai-input-text-color: #e5e7eb;
902
+ --bonsai-suggestions-hover-bg: rgba(255, 255, 255, 0.06);
903
+ --bonsai-muted-color: #a1a1aa;
904
+ --bonsai-input-bg: #1f2229;
905
+ }
906
+ `;
907
+ }
908
+ };
909
+ if (typeof window !== "undefined" && !customElements.get("bonsai-searchbar")) customElements.define("bonsai-searchbar", BonsaiSearchBarElement);
910
+
911
+ //#endregion
912
+ //#region src/index.ts
913
+ const VERSION = "3.0.8";
914
+ const ICONS = {
915
+ search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
916
+ <circle cx="11" cy="11" r="8"></circle>
917
+ <path d="m21 21-4.35-4.35"></path>
918
+ </svg>`,
919
+ arrowRight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
920
+ <path d="M5 12h14"></path>
921
+ <path d="m12 5 7 7-7 7"></path>
922
+ </svg>`,
923
+ check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
924
+ <polyline points="20 6 9 17 4 12"></polyline>
925
+ </svg>`,
926
+ reset: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
927
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
928
+ <path d="M3 3v5h5"></path>
929
+ </svg>`,
930
+ spinner: `<svg viewBox="0 0 24 24">
931
+ <circle class="bonsai-spinner-track" cx="12" cy="12" r="10" stroke-width="3" fill="none"></circle>
932
+ <path class="bonsai-spinner-fill" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
933
+ </svg>`
934
+ };
935
+ var BonsaiSearch = class {
936
+ config;
937
+ state = "default";
938
+ container;
939
+ searchBar = null;
940
+ searchInput = null;
941
+ actionsContainer = null;
942
+ submitBtn = null;
943
+ loadingState = null;
944
+ loadingTextInner = null;
945
+ doneState = null;
946
+ resetBtn = null;
947
+ suggestionsDropdown = null;
948
+ poweredBy = null;
949
+ resultsContainer = null;
950
+ query = "";
951
+ loadingIndex = 0;
952
+ loadingInterval = null;
953
+ debounceTimer = null;
954
+ abortController = null;
955
+ loadingStates = [
956
+ "Searching",
957
+ "Analyzing",
958
+ "Curating"
959
+ ];
960
+ constructor(config) {
961
+ this.container = config.container;
962
+ this.config = {
963
+ container: config.container,
964
+ apiKey: config.apiKey,
965
+ baseUrl: config.baseUrl || this.inferBaseUrl(),
966
+ placeholder: config.placeholder || "Describe what you're looking for...",
967
+ suggestions: config.suggestions || [],
968
+ debounceMs: config.debounceMs ?? 0,
969
+ maxResults: config.maxResults ?? 50,
970
+ timeoutMs: config.timeoutMs ?? 3e4,
971
+ imageHostname: config.imageHostname || "",
972
+ generateSrcSet: config.generateSrcSet ?? false,
973
+ srcSetSizes: config.srcSetSizes || [],
974
+ classAdditions: config.classAdditions || {},
975
+ renderPrice: config.renderPrice ?? false,
976
+ renderPriceWithTitle: config.renderPriceWithTitle ?? true,
977
+ imageObjectFit: config.imageObjectFit || "cover",
978
+ markdown: config.markdown ?? false,
979
+ featuredItemsLabel: config.featuredItemsLabel || "Featured Items",
980
+ moreItemsLabel: config.moreItemsLabel || "More Items",
981
+ itemsLabel: config.itemsLabel || "Items",
982
+ onPrice: config.onPrice ?? (() => ""),
983
+ onImg: config.onImg ?? (() => {}),
984
+ onSearch: config.onSearch ?? (() => {}),
985
+ onResults: config.onResults ?? (() => {}),
986
+ onAi: config.onAi ?? (() => {}),
987
+ onError: config.onError ?? (() => {})
988
+ };
989
+ this.validateConfig();
990
+ this.render();
991
+ this.attachEventListeners();
992
+ this.checkForQueryParam();
993
+ }
994
+ /** Get the SDK version */
995
+ static get version() {
996
+ return VERSION;
997
+ }
998
+ /** Infer base URL from script tag or use current origin */
999
+ inferBaseUrl() {
1000
+ const script = document.querySelector("script[src*=\"bonsai-search\"]");
1001
+ if (script?.src) {
1002
+ const url = new URL(script.src);
1003
+ return `${url.protocol}//${url.host}`;
1004
+ }
1005
+ return window.location.origin;
1006
+ }
1007
+ /** Build class name string with any configured additional classes */
1008
+ buildClassName(baseClass) {
1009
+ const additions = this.config.classAdditions[baseClass];
1010
+ if (additions && additions.length > 0) return `${baseClass} ${additions.join(" ")}`;
1011
+ return baseClass;
1012
+ }
1013
+ /** Apply a shadow part name for external styling */
1014
+ applyPart(el, part) {
1015
+ el.setAttribute("part", part);
1016
+ }
1017
+ /** Validate configuration */
1018
+ validateConfig() {
1019
+ if (!this.config.container) throw new Error("BonsaiSearch: container element is required");
1020
+ if (!this.config.apiKey) throw new Error("BonsaiSearch: apiKey is required");
1021
+ }
1022
+ /** Render the search UI */
1023
+ render() {
1024
+ this.container.innerHTML = "";
1025
+ this.container.classList.add("bonsai-search-container");
1026
+ const wrapper = document.createElement("div");
1027
+ wrapper.className = this.buildClassName("bonsai-search-wrapper");
1028
+ this.applyPart(wrapper, "search-wrapper");
1029
+ this.searchBar = document.createElement("div");
1030
+ this.searchBar.className = this.buildClassName("bonsai-search-bar");
1031
+ this.applyPart(this.searchBar, "search-bar");
1032
+ const searchIcon = document.createElement("div");
1033
+ searchIcon.className = this.buildClassName("bonsai-search-icon");
1034
+ this.applyPart(searchIcon, "search-icon");
1035
+ searchIcon.innerHTML = ICONS.search;
1036
+ this.searchBar.appendChild(searchIcon);
1037
+ this.searchInput = document.createElement("input");
1038
+ this.searchInput.type = "text";
1039
+ this.searchInput.className = this.buildClassName("bonsai-search-input");
1040
+ this.applyPart(this.searchInput, "search-input");
1041
+ this.searchInput.placeholder = this.config.placeholder;
1042
+ this.searchInput.setAttribute("aria-label", "Search input");
1043
+ this.searchBar.appendChild(this.searchInput);
1044
+ this.actionsContainer = document.createElement("div");
1045
+ this.actionsContainer.className = this.buildClassName("bonsai-search-actions");
1046
+ this.applyPart(this.actionsContainer, "search-actions");
1047
+ this.submitBtn = document.createElement("button");
1048
+ this.submitBtn.type = "button";
1049
+ this.submitBtn.className = this.buildClassName("bonsai-submit-btn");
1050
+ this.applyPart(this.submitBtn, "submit-button");
1051
+ this.submitBtn.setAttribute("aria-label", "Submit search");
1052
+ this.submitBtn.innerHTML = ICONS.arrowRight;
1053
+ this.actionsContainer.appendChild(this.submitBtn);
1054
+ this.loadingState = document.createElement("div");
1055
+ this.loadingState.className = this.buildClassName("bonsai-loading-state");
1056
+ this.applyPart(this.loadingState, "loading-state");
1057
+ const loadingText = document.createElement("div");
1058
+ loadingText.className = this.buildClassName("bonsai-loading-text");
1059
+ this.loadingTextInner = document.createElement("div");
1060
+ this.loadingTextInner.className = this.buildClassName("bonsai-loading-text-inner");
1061
+ this.applyPart(loadingText, "loading-text");
1062
+ this.applyPart(this.loadingTextInner, "loading-text-inner");
1063
+ this.loadingStates.forEach((text) => {
1064
+ const span = document.createElement("span");
1065
+ span.textContent = text;
1066
+ this.loadingTextInner.appendChild(span);
1067
+ });
1068
+ loadingText.appendChild(this.loadingTextInner);
1069
+ this.loadingState.appendChild(loadingText);
1070
+ const spinner = document.createElement("div");
1071
+ spinner.className = this.buildClassName("bonsai-spinner");
1072
+ this.applyPart(spinner, "spinner");
1073
+ spinner.innerHTML = ICONS.spinner;
1074
+ this.loadingState.appendChild(spinner);
1075
+ this.actionsContainer.appendChild(this.loadingState);
1076
+ this.doneState = document.createElement("div");
1077
+ this.doneState.className = this.buildClassName("bonsai-done-state");
1078
+ this.applyPart(this.doneState, "done-state");
1079
+ const doneText = document.createElement("span");
1080
+ doneText.className = this.buildClassName("bonsai-done-text");
1081
+ this.applyPart(doneText, "done-text");
1082
+ doneText.textContent = "Done!";
1083
+ this.doneState.appendChild(doneText);
1084
+ const doneIcon = document.createElement("div");
1085
+ doneIcon.className = this.buildClassName("bonsai-done-icon");
1086
+ this.applyPart(doneIcon, "done-icon");
1087
+ doneIcon.innerHTML = ICONS.check;
1088
+ this.doneState.appendChild(doneIcon);
1089
+ this.actionsContainer.appendChild(this.doneState);
1090
+ this.resetBtn = document.createElement("button");
1091
+ this.resetBtn.type = "button";
1092
+ this.resetBtn.className = this.buildClassName("bonsai-reset-btn");
1093
+ this.applyPart(this.resetBtn, "reset-button");
1094
+ this.resetBtn.setAttribute("aria-label", "Reset search");
1095
+ const resetText = document.createElement("span");
1096
+ resetText.className = this.buildClassName("bonsai-reset-text");
1097
+ this.applyPart(resetText, "reset-text");
1098
+ resetText.textContent = "Reset";
1099
+ this.resetBtn.appendChild(resetText);
1100
+ const resetIcon = document.createElement("div");
1101
+ resetIcon.className = this.buildClassName("bonsai-reset-icon");
1102
+ this.applyPart(resetIcon, "reset-icon");
1103
+ resetIcon.innerHTML = ICONS.reset;
1104
+ this.resetBtn.appendChild(resetIcon);
1105
+ this.actionsContainer.appendChild(this.resetBtn);
1106
+ this.searchBar.appendChild(this.actionsContainer);
1107
+ if (this.config.suggestions.length > 0) {
1108
+ this.suggestionsDropdown = document.createElement("div");
1109
+ this.suggestionsDropdown.className = this.buildClassName("bonsai-suggestions");
1110
+ this.applyPart(this.suggestionsDropdown, "suggestions");
1111
+ this.suggestionsDropdown.id = "bonsai-suggestions";
1112
+ this.suggestionsDropdown.setAttribute("role", "listbox");
1113
+ this.renderSuggestions();
1114
+ this.searchBar.appendChild(this.suggestionsDropdown);
1115
+ }
1116
+ wrapper.appendChild(this.searchBar);
1117
+ this.poweredBy = document.createElement("div");
1118
+ this.poweredBy.className = this.buildClassName("bonsai-powered-by");
1119
+ this.applyPart(this.poweredBy, "powered-by");
1120
+ this.poweredBy.innerHTML = `powered by <a href="https://www.hibonsai.com/" target="_blank" rel="noopener">Bonsai<span class="bonsai-powered-by-dot"></span></a>`;
1121
+ wrapper.appendChild(this.poweredBy);
1122
+ this.resultsContainer = document.createElement("div");
1123
+ this.resultsContainer.className = this.buildClassName("bonsai-results");
1124
+ this.applyPart(this.resultsContainer, "results");
1125
+ wrapper.appendChild(this.resultsContainer);
1126
+ this.container.appendChild(wrapper);
1127
+ }
1128
+ /** Render suggestions in the dropdown */
1129
+ renderSuggestions() {
1130
+ if (!this.suggestionsDropdown) return;
1131
+ this.suggestionsDropdown.innerHTML = "";
1132
+ this.config.suggestions.slice(0, 5).forEach((suggestion) => {
1133
+ const item = document.createElement("button");
1134
+ item.type = "button";
1135
+ item.className = this.buildClassName("bonsai-suggestion-item");
1136
+ this.applyPart(item, "suggestion-item");
1137
+ item.setAttribute("role", "option");
1138
+ const text = document.createElement("span");
1139
+ text.textContent = suggestion;
1140
+ this.applyPart(text, "suggestion-text");
1141
+ item.appendChild(text);
1142
+ const arrow = document.createElement("div");
1143
+ arrow.className = this.buildClassName("bonsai-suggestion-arrow");
1144
+ this.applyPart(arrow, "suggestion-arrow");
1145
+ arrow.innerHTML = ICONS.arrowRight;
1146
+ item.appendChild(arrow);
1147
+ item.addEventListener("click", () => this.handleSuggestionClick(suggestion));
1148
+ this.suggestionsDropdown.appendChild(item);
1149
+ });
1150
+ }
1151
+ /** Attach event listeners */
1152
+ attachEventListeners() {
1153
+ if (!this.searchInput) return;
1154
+ this.searchInput.addEventListener("focus", () => this.handleFocus());
1155
+ this.searchInput.addEventListener("blur", () => this.handleBlur());
1156
+ this.searchInput.addEventListener("input", (e) => this.handleInput(e));
1157
+ this.searchInput.addEventListener("keydown", (e) => this.handleKeyDown(e));
1158
+ this.submitBtn?.addEventListener("click", (e) => {
1159
+ e.preventDefault();
1160
+ e.stopPropagation();
1161
+ this.handleSubmit();
1162
+ });
1163
+ this.resetBtn?.addEventListener("click", (e) => {
1164
+ e.preventDefault();
1165
+ e.stopPropagation();
1166
+ this.handleReset();
1167
+ });
1168
+ }
1169
+ /** Handle input focus */
1170
+ handleFocus() {
1171
+ if (this.state === "default" || this.state === "retry") {
1172
+ if (!this.query && this.searchInput) this.searchInput.value = "";
1173
+ else if (this.query && this.searchInput) this.searchInput.value = this.query;
1174
+ if (!this.query) this.setState("focused");
1175
+ }
1176
+ this.searchBar?.classList.add("bonsai-focused");
1177
+ if (!this.query && this.suggestionsDropdown) {
1178
+ this.renderSuggestions();
1179
+ this.suggestionsDropdown.classList.add("bonsai-visible");
1180
+ }
1181
+ }
1182
+ /** Handle input blur */
1183
+ handleBlur() {
1184
+ this.searchBar?.classList.remove("bonsai-focused");
1185
+ setTimeout(() => {
1186
+ this.suggestionsDropdown?.classList.remove("bonsai-visible");
1187
+ }, 200);
1188
+ }
1189
+ /** Handle input change */
1190
+ handleInput(e) {
1191
+ this.query = e.target.value;
1192
+ if (this.debounceTimer !== null) {
1193
+ window.clearTimeout(this.debounceTimer);
1194
+ this.debounceTimer = null;
1195
+ }
1196
+ if (this.query) {
1197
+ if (this.state === "retry" || this.state === "done") this.setState("filled");
1198
+ else if (this.state !== "loading") this.setState("filled");
1199
+ this.suggestionsDropdown?.classList.remove("bonsai-visible");
1200
+ } else {
1201
+ if (this.state !== "loading" && this.state !== "done" && this.state !== "retry") this.setState("focused");
1202
+ if (this.suggestionsDropdown) {
1203
+ this.renderSuggestions();
1204
+ this.suggestionsDropdown.classList.add("bonsai-visible");
1205
+ }
1206
+ }
1207
+ }
1208
+ /** Handle key down */
1209
+ handleKeyDown(e) {
1210
+ if (e.key === "Enter" && this.query) {
1211
+ e.preventDefault();
1212
+ if (this.debounceTimer !== null) {
1213
+ window.clearTimeout(this.debounceTimer);
1214
+ this.debounceTimer = null;
1215
+ }
1216
+ this.handleSubmit();
1217
+ }
1218
+ }
1219
+ /** Handle suggestion click */
1220
+ handleSuggestionClick(suggestion) {
1221
+ this.query = suggestion;
1222
+ if (this.searchInput) this.searchInput.value = suggestion;
1223
+ this.suggestionsDropdown?.classList.remove("bonsai-visible");
1224
+ this.handleSubmit();
1225
+ }
1226
+ /** Handle search submit using Server-Sent Events */
1227
+ async handleSubmit() {
1228
+ if (!this.query || !this.query.trim()) return;
1229
+ if (this.state === "loading") return;
1230
+ if (this.debounceTimer !== null) {
1231
+ window.clearTimeout(this.debounceTimer);
1232
+ this.debounceTimer = null;
1233
+ }
1234
+ if (this.abortController) this.abortController.abort();
1235
+ this.abortController = new AbortController();
1236
+ this.setState("loading");
1237
+ this.config.onSearch(this.query);
1238
+ this.startLoadingAnimation();
1239
+ let initialResults = [];
1240
+ let hasReceivedResults = false;
1241
+ let hasReceivedAi = false;
1242
+ try {
1243
+ this.resultsContainer.innerHTML = "";
1244
+ const url = new URL(this.config.baseUrl);
1245
+ url.searchParams.set("q", this.query);
1246
+ if (this.config.markdown) url.searchParams.set("markdown", "true");
1247
+ const response = await fetch(url.toString(), {
1248
+ method: "GET",
1249
+ headers: {
1250
+ Accept: "text/event-stream",
1251
+ "X-API-Key": this.config.apiKey
1252
+ },
1253
+ signal: this.abortController.signal
1254
+ });
1255
+ if (!response.ok) {
1256
+ const errorData = await response.json().catch(() => ({}));
1257
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
1258
+ }
1259
+ const reader = response.body?.getReader();
1260
+ if (!reader) throw new Error("Unable to read response stream");
1261
+ const decoder = new TextDecoder();
1262
+ let buffer = "";
1263
+ while (true) {
1264
+ const { done, value } = await reader.read();
1265
+ if (done) break;
1266
+ buffer += decoder.decode(value, { stream: true });
1267
+ const events = this.parseSSEEvents(buffer);
1268
+ buffer = events.remaining;
1269
+ for (const event of events.parsed) if (event.type === "results") {
1270
+ hasReceivedResults = true;
1271
+ const data = event.data;
1272
+ if (!data.success && data.error) throw new Error(data.error);
1273
+ initialResults = (data.results || []).filter((r) => r.publicUrl && r.publicUrl.trim() !== "");
1274
+ initialResults = this.config.maxResults ? initialResults.slice(0, this.config.maxResults) : initialResults;
1275
+ this.renderResults([], initialResults);
1276
+ this.showRecommendationsLoading();
1277
+ this.config.onResults(initialResults);
1278
+ } else if (event.type === "ai") {
1279
+ hasReceivedAi = true;
1280
+ const data = event.data;
1281
+ let finalResults = (data.results || []).filter((r) => r.publicUrl && r.publicUrl.trim() !== "");
1282
+ finalResults = this.config.maxResults ? finalResults.slice(0, this.config.maxResults) : finalResults;
1283
+ this.hideRecommendationsLoading();
1284
+ let resultsToRender = finalResults;
1285
+ if (finalResults.length === 0) {
1286
+ const recProductIds = (data.recommendations || []).flatMap((r) => r.productIds || []);
1287
+ if (recProductIds.length > 0) {
1288
+ const matched = recProductIds.map((id) => initialResults.find((r) => r.id === id)).filter((r) => r != null);
1289
+ resultsToRender = matched.length > 0 ? matched : initialResults;
1290
+ } else resultsToRender = initialResults;
1291
+ }
1292
+ this.renderResults(data.recommendations || [], resultsToRender);
1293
+ this.config.onAi(data);
1294
+ this.stopLoadingAnimation();
1295
+ this.setState("done");
1296
+ setTimeout(() => {
1297
+ if (this.state === "done") this.setState("retry");
1298
+ }, 2e3);
1299
+ }
1300
+ }
1301
+ if (hasReceivedResults && !hasReceivedAi) {
1302
+ this.stopLoadingAnimation();
1303
+ this.setState("done");
1304
+ setTimeout(() => {
1305
+ if (this.state === "done") this.setState("retry");
1306
+ }, 2e3);
1307
+ }
1308
+ } catch (error) {
1309
+ if (error instanceof Error) {
1310
+ if (error.name === "AbortError") return;
1311
+ this.renderError(error.message);
1312
+ this.config.onError(error);
1313
+ }
1314
+ this.setState("retry");
1315
+ this.stopLoadingAnimation();
1316
+ }
1317
+ }
1318
+ /** Parse SSE events from a buffer string */
1319
+ parseSSEEvents(buffer) {
1320
+ const parsed = [];
1321
+ const lines = buffer.split("\n");
1322
+ let currentEvent = {};
1323
+ let remaining = "";
1324
+ for (let i = 0; i < lines.length; i++) {
1325
+ const line = lines[i];
1326
+ if (i === lines.length - 1 && line !== "") {
1327
+ remaining = line;
1328
+ continue;
1329
+ }
1330
+ if (line.startsWith("event: ")) currentEvent.type = line.slice(7).trim();
1331
+ else if (line.startsWith("data: ")) currentEvent.data = line.slice(6);
1332
+ else if (line === "" && currentEvent.type && currentEvent.data) {
1333
+ try {
1334
+ const data = JSON.parse(currentEvent.data);
1335
+ parsed.push({
1336
+ type: currentEvent.type,
1337
+ data
1338
+ });
1339
+ } catch (e) {
1340
+ console.error("Failed to parse SSE data:", e);
1341
+ }
1342
+ currentEvent = {};
1343
+ }
1344
+ }
1345
+ if (currentEvent.type || currentEvent.data) {
1346
+ if (currentEvent.type) remaining = `event: ${currentEvent.type}\n` + remaining;
1347
+ if (currentEvent.data) remaining = (remaining ? remaining + "\n" : "") + `data: ${currentEvent.data}`;
1348
+ }
1349
+ return {
1350
+ parsed,
1351
+ remaining
1352
+ };
1353
+ }
1354
+ /** Handle reset */
1355
+ handleReset() {
1356
+ this.query = "";
1357
+ if (this.searchInput) this.searchInput.value = "";
1358
+ this.setState("focused");
1359
+ this.searchInput?.focus();
1360
+ if (this.resultsContainer) this.resultsContainer.classList.remove("bonsai-visible");
1361
+ if (this.suggestionsDropdown) {
1362
+ this.renderSuggestions();
1363
+ this.suggestionsDropdown.classList.add("bonsai-visible");
1364
+ }
1365
+ }
1366
+ /** Set the current state and update UI */
1367
+ setState(newState) {
1368
+ this.state = newState;
1369
+ this.updateActionButtons();
1370
+ this.updateInputState();
1371
+ }
1372
+ /** Update action buttons visibility based on state */
1373
+ updateActionButtons() {
1374
+ this.submitBtn?.classList.remove("bonsai-visible");
1375
+ this.loadingState?.classList.remove("bonsai-visible");
1376
+ this.doneState?.classList.remove("bonsai-visible");
1377
+ this.resetBtn?.classList.remove("bonsai-visible");
1378
+ switch (this.state) {
1379
+ case "filled":
1380
+ this.submitBtn?.classList.add("bonsai-visible");
1381
+ break;
1382
+ case "loading":
1383
+ this.loadingState?.classList.add("bonsai-visible");
1384
+ break;
1385
+ case "done":
1386
+ this.doneState?.classList.add("bonsai-visible");
1387
+ break;
1388
+ case "retry":
1389
+ this.resetBtn?.classList.add("bonsai-visible");
1390
+ break;
1391
+ }
1392
+ }
1393
+ /** Update input state (disabled only during loading) */
1394
+ updateInputState() {
1395
+ if (!this.searchInput) return;
1396
+ const isDisabled = this.state === "loading";
1397
+ const wasDisabled = this.searchInput.disabled;
1398
+ this.searchInput.disabled = isDisabled;
1399
+ if (wasDisabled && !isDisabled && this.query) this.searchInput.value = this.query;
1400
+ }
1401
+ /** Start loading animation */
1402
+ startLoadingAnimation() {
1403
+ this.loadingIndex = 0;
1404
+ this.updateLoadingText();
1405
+ this.loadingInterval = window.setInterval(() => {
1406
+ if (this.loadingIndex < this.loadingStates.length - 1) {
1407
+ this.loadingIndex++;
1408
+ this.updateLoadingText();
1409
+ }
1410
+ }, 1e3);
1411
+ }
1412
+ /** Update loading text position */
1413
+ updateLoadingText() {
1414
+ if (this.loadingTextInner) {
1415
+ const offset = this.loadingIndex * 1.25;
1416
+ this.loadingTextInner.style.transform = `translateY(-${offset}rem)`;
1417
+ }
1418
+ }
1419
+ /** Stop loading animation */
1420
+ stopLoadingAnimation() {
1421
+ if (this.loadingInterval !== null) {
1422
+ window.clearInterval(this.loadingInterval);
1423
+ this.loadingInterval = null;
1424
+ }
1425
+ }
1426
+ /** Show recommendations loading indicator */
1427
+ showRecommendationsLoading() {
1428
+ if (!this.resultsContainer) return;
1429
+ this.hideRecommendationsLoading();
1430
+ const loadingIndicator = document.createElement("div");
1431
+ loadingIndicator.className = this.buildClassName("bonsai-recommendations-loading");
1432
+ this.applyPart(loadingIndicator, "recommendations-loading");
1433
+ loadingIndicator.innerHTML = `
1434
+ <div class="${this.buildClassName("bonsai-recommendations-loading-spinner")}">
1435
+ ${ICONS.spinner}
1436
+ </div>
1437
+ <span class="${this.buildClassName("bonsai-recommendations-loading-text")}">Loading recommendations...</span>
1438
+ `;
1439
+ const loadingSpinner = loadingIndicator.querySelector(".bonsai-recommendations-loading-spinner");
1440
+ if (loadingSpinner) this.applyPart(loadingSpinner, "recommendations-loading-spinner");
1441
+ const loadingText = loadingIndicator.querySelector(".bonsai-recommendations-loading-text");
1442
+ if (loadingText) this.applyPart(loadingText, "recommendations-loading-text");
1443
+ this.resultsContainer.insertBefore(loadingIndicator, this.resultsContainer.firstChild);
1444
+ }
1445
+ /** Hide recommendations loading indicator */
1446
+ hideRecommendationsLoading() {
1447
+ if (!this.resultsContainer) return;
1448
+ const loadingIndicator = this.resultsContainer.querySelector(".bonsai-recommendations-loading");
1449
+ if (loadingIndicator) loadingIndicator.remove();
1450
+ }
1451
+ /** Render search results */
1452
+ renderResults(recommendations, results) {
1453
+ if (!this.resultsContainer) return;
1454
+ if (results.length === 0 && recommendations.length === 0) {
1455
+ this.renderEmpty();
1456
+ return;
1457
+ }
1458
+ const recommendedProductIds = /* @__PURE__ */ new Set();
1459
+ for (const rec of recommendations) if (rec.productIds) for (const id of rec.productIds) recommendedProductIds.add(id);
1460
+ if (recommendations.length > 0) for (const rec of recommendations) {
1461
+ const summarySection = document.createElement("div");
1462
+ summarySection.className = this.buildClassName("bonsai-summary");
1463
+ this.applyPart(summarySection, "summary");
1464
+ const summaryText = document.createElement(this.config.markdown ? "div" : "p");
1465
+ summaryText.className = this.buildClassName("bonsai-summary-text");
1466
+ this.applyPart(summaryText, "summary-text");
1467
+ if (this.config.markdown) summaryText.innerHTML = marked.parse(rec.text, { async: false });
1468
+ else summaryText.textContent = rec.text;
1469
+ summarySection.appendChild(summaryText);
1470
+ if (rec.productIds && rec.productIds.length > 0) {
1471
+ const uniqueProductIds = [...new Set(rec.productIds)];
1472
+ const recResults = [];
1473
+ for (const prodId of uniqueProductIds) {
1474
+ const product = results.find((r) => r.id === prodId);
1475
+ if (product && product.publicUrl && product.publicUrl.trim() !== "") recResults.push(product);
1476
+ }
1477
+ const recProducts = document.createElement("div");
1478
+ const recGrid = document.createElement("div");
1479
+ if (recResults.length > 0) {
1480
+ recProducts.className = this.buildClassName("bonsai-recommendation-products");
1481
+ this.applyPart(recProducts, "recommendation-products");
1482
+ const recHeader = document.createElement("h4");
1483
+ recHeader.className = this.buildClassName("bonsai-recommendation-products-header");
1484
+ this.applyPart(recHeader, "recommendation-products-header");
1485
+ recHeader.textContent = this.config.featuredItemsLabel;
1486
+ recProducts.appendChild(recHeader);
1487
+ recGrid.className = this.buildClassName("bonsai-results-grid");
1488
+ this.applyPart(recGrid, "results-grid");
1489
+ for (const product of recResults) {
1490
+ const card = this.createResultCard(product);
1491
+ recGrid.appendChild(card);
1492
+ }
1493
+ }
1494
+ recProducts.appendChild(recGrid);
1495
+ summarySection.appendChild(recProducts);
1496
+ }
1497
+ this.resultsContainer.prepend(summarySection);
1498
+ }
1499
+ else if (results.length > 0) {
1500
+ const resultsSection = document.createElement("div");
1501
+ this.applyPart(resultsSection, "results-section");
1502
+ const resultsHeader = document.createElement("h3");
1503
+ resultsHeader.className = this.buildClassName("bonsai-results-header");
1504
+ this.applyPart(resultsHeader, "results-header");
1505
+ resultsHeader.textContent = recommendedProductIds.size > 0 ? this.config.moreItemsLabel : this.config.itemsLabel;
1506
+ resultsSection.appendChild(resultsHeader);
1507
+ const resultsGrid = document.createElement("div");
1508
+ resultsGrid.className = this.buildClassName("bonsai-results-grid");
1509
+ this.applyPart(resultsGrid, "results-grid");
1510
+ for (const result of results) {
1511
+ const card = this.createResultCard(result);
1512
+ resultsGrid.appendChild(card);
1513
+ }
1514
+ resultsSection.appendChild(resultsGrid);
1515
+ this.resultsContainer.appendChild(resultsSection);
1516
+ }
1517
+ this.resultsContainer.classList.add("bonsai-visible");
1518
+ setTimeout(() => {
1519
+ this.resultsContainer.querySelectorAll(".bonsai-result-card").forEach((card, index) => {
1520
+ setTimeout(() => {
1521
+ card.classList.add("bonsai-animate-in");
1522
+ }, index * 100);
1523
+ });
1524
+ }, 50);
1525
+ }
1526
+ /** Generate srcset attribute for an image URL */
1527
+ getSrcSet(url) {
1528
+ if (!this.config.generateSrcSet || !this.config.imageHostname || this.config.srcSetSizes.length === 0) return "";
1529
+ const filename = url.split("/").pop();
1530
+ if (!filename) return "";
1531
+ let baseUrl = `//${this.config.imageHostname}/cdn/shop/`;
1532
+ if (url.includes("/products/")) baseUrl += "products/";
1533
+ else baseUrl += "files/";
1534
+ const fullPath = baseUrl + filename;
1535
+ const srcSetEntries = [];
1536
+ for (const size of this.config.srcSetSizes) {
1537
+ const entry = fullPath.replace(/(\.\w+)(\?v=\d+)?$/, `_${size.width}x${size.height}_${size.crop}$1$2 ${size.width}w`);
1538
+ srcSetEntries.push(entry);
1539
+ }
1540
+ return srcSetEntries.join(", ");
1541
+ }
1542
+ /** Resolve an image value that may be a string URL or an object with sourceUrl/source_url */
1543
+ resolveImageUrl(image) {
1544
+ let url = image;
1545
+ if (url && typeof url === "object") {
1546
+ const obj = url;
1547
+ url = obj.sourceUrl || obj.source_url || null;
1548
+ }
1549
+ if (typeof url !== "string" || url.length === 0) return null;
1550
+ try {
1551
+ const parsed = new URL(url);
1552
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return url;
1553
+ } catch {}
1554
+ return null;
1555
+ }
1556
+ /** Create a result card */
1557
+ createResultCard(result) {
1558
+ const card = document.createElement("a");
1559
+ card.href = result.publicUrl;
1560
+ card.className = this.buildClassName("bonsai-result-card");
1561
+ this.applyPart(card, "result-card");
1562
+ card.target = "_blank";
1563
+ card.rel = "noopener";
1564
+ const imageUrl = this.resolveImageUrl(result.image);
1565
+ if (imageUrl) {
1566
+ const imgWrapper = document.createElement("div");
1567
+ imgWrapper.className = this.buildClassName("bonsai-result-image-wrapper");
1568
+ this.applyPart(imgWrapper, "result-image-wrapper");
1569
+ card.appendChild(imgWrapper);
1570
+ const img = document.createElement("img");
1571
+ img.className = this.buildClassName("bonsai-result-image");
1572
+ this.applyPart(img, "result-image");
1573
+ img.src = imageUrl;
1574
+ img.alt = result.name;
1575
+ img.loading = "lazy";
1576
+ img.style.objectFit = this.config.imageObjectFit;
1577
+ const srcset = this.getSrcSet(imageUrl);
1578
+ if (srcset) img.srcset = srcset;
1579
+ this.config.onImg(result, img, imgWrapper, this);
1580
+ imgWrapper.appendChild(img);
1581
+ }
1582
+ const content = document.createElement("div");
1583
+ content.className = this.buildClassName("bonsai-result-content");
1584
+ this.applyPart(content, "result-content");
1585
+ const title = document.createElement("h4");
1586
+ title.className = this.buildClassName("bonsai-result-title");
1587
+ this.applyPart(title, "result-title");
1588
+ title.textContent = result.name;
1589
+ content.appendChild(title);
1590
+ if (this.config.renderPrice && result.price) {
1591
+ const priceContainer = document.createElement("div");
1592
+ priceContainer.className = this.buildClassName("bonsai-result-price-container");
1593
+ this.applyPart(priceContainer, "result-price-container");
1594
+ const price = document.createElement("span");
1595
+ price.className = this.buildClassName("bonsai-result-price");
1596
+ this.applyPart(price, "result-price");
1597
+ price.textContent = this.config.onPrice(result, result.price, priceContainer);
1598
+ priceContainer.appendChild(price);
1599
+ if (result.compareAtPrice && result.compareAtPrice !== result.price) {
1600
+ const compareAt = document.createElement("span");
1601
+ compareAt.className = this.buildClassName("bonsai-result-compare-at");
1602
+ this.applyPart(compareAt, "result-compare-at");
1603
+ compareAt.textContent = this.config.onPrice(result, result.compareAtPrice);
1604
+ priceContainer.appendChild(compareAt);
1605
+ }
1606
+ content.appendChild(priceContainer);
1607
+ }
1608
+ if (result.description) {
1609
+ const caption = document.createElement("p");
1610
+ caption.className = this.buildClassName("bonsai-result-caption");
1611
+ this.applyPart(caption, "result-caption");
1612
+ caption.textContent = result.description;
1613
+ content.appendChild(caption);
1614
+ } else if (!this.config.renderPrice && result.price) {
1615
+ const caption = document.createElement("p");
1616
+ caption.className = this.buildClassName("bonsai-result-caption");
1617
+ this.applyPart(caption, "result-caption");
1618
+ caption.textContent = this.config.onPrice(result, result.price, caption);
1619
+ content.appendChild(caption);
1620
+ }
1621
+ card.appendChild(content);
1622
+ return card;
1623
+ }
1624
+ /** Render empty state */
1625
+ renderEmpty() {
1626
+ if (!this.resultsContainer) return;
1627
+ const empty = document.createElement("p");
1628
+ empty.className = this.buildClassName("bonsai-empty-state");
1629
+ this.applyPart(empty, "empty-state");
1630
+ empty.textContent = "No matching products found. Try a different search query.";
1631
+ this.resultsContainer.appendChild(empty);
1632
+ this.resultsContainer.classList.add("bonsai-visible");
1633
+ }
1634
+ /** Render error state */
1635
+ renderError(message) {
1636
+ if (!this.resultsContainer) return;
1637
+ this.resultsContainer.innerHTML = "";
1638
+ const error = document.createElement("div");
1639
+ error.className = this.buildClassName("bonsai-error");
1640
+ this.applyPart(error, "error");
1641
+ error.textContent = `Error: ${message}`;
1642
+ this.resultsContainer.appendChild(error);
1643
+ this.resultsContainer.classList.add("bonsai-visible");
1644
+ }
1645
+ /** Check for query parameter in URL or localStorage */
1646
+ checkForQueryParam() {
1647
+ const urlParams = new URLSearchParams(window.location.search);
1648
+ let queryParam = urlParams.get("q") || urlParams.get("query");
1649
+ if (!queryParam) try {
1650
+ const storedQuery = localStorage.getItem("bonsai-search-query");
1651
+ if (storedQuery) {
1652
+ queryParam = storedQuery;
1653
+ localStorage.removeItem("bonsai-search-query");
1654
+ }
1655
+ } catch {}
1656
+ if (queryParam && this.searchInput) {
1657
+ this.searchInput.value = queryParam;
1658
+ this.query = queryParam;
1659
+ this.handleSubmit();
1660
+ }
1661
+ }
1662
+ /** Set a suggestion programmatically and trigger search */
1663
+ setSuggestion(suggestion) {
1664
+ if (this.searchInput) {
1665
+ this.searchInput.value = suggestion;
1666
+ this.query = suggestion;
1667
+ this.handleSubmit();
1668
+ }
1669
+ }
1670
+ /** Clear the search and results */
1671
+ clear() {
1672
+ this.handleReset();
1673
+ if (this.resultsContainer) {
1674
+ this.resultsContainer.innerHTML = "";
1675
+ this.resultsContainer.classList.remove("bonsai-visible");
1676
+ }
1677
+ }
1678
+ /** Focus the search input */
1679
+ focus() {
1680
+ this.searchInput?.focus();
1681
+ }
1682
+ /** Destroy the instance and clean up */
1683
+ destroy() {
1684
+ if (this.abortController) this.abortController.abort();
1685
+ if (this.debounceTimer !== null) window.clearTimeout(this.debounceTimer);
1686
+ if (this.loadingInterval !== null) window.clearInterval(this.loadingInterval);
1687
+ this.container.innerHTML = "";
1688
+ }
1689
+ };
1690
+ if (typeof window !== "undefined") window.BonsaiSearch = BonsaiSearch;
1691
+
1692
+ //#endregion
1693
+ export { BonsaiSearch, BonsaiSearchBarElement, BonsaiSearchElement };