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.
@@ -0,0 +1,486 @@
1
+ (function(exports) {
2
+
3
+
4
+ //#region src/searchbar.ts
5
+ const VERSION = "3.0.8";
6
+ const ICONS = {
7
+ search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8
+ <circle cx="11" cy="11" r="8"></circle>
9
+ <path d="m21 21-4.35-4.35"></path>
10
+ </svg>`,
11
+ arrowRight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
12
+ <path d="M5 12h14"></path>
13
+ <path d="m12 5 7 7-7 7"></path>
14
+ </svg>`,
15
+ close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
16
+ <path d="M18 6L6 18"></path>
17
+ <path d="M6 6l12 12"></path>
18
+ </svg>`
19
+ };
20
+ const SEARCHBAR_STYLES = `
21
+ @keyframes bonsai-searchbar-fade-in {
22
+ from { opacity: 0; transform: translateY(-10px); }
23
+ to { opacity: 1; transform: translateY(0); }
24
+ }
25
+ @keyframes bonsai-searchbar-scale-in {
26
+ from { opacity: 0; transform: scale(0.8); }
27
+ to { opacity: 1; transform: scale(1); }
28
+ }
29
+ .bonsai-searchbar-container {
30
+ margin: 24px auto;
31
+ font-family: system-ui, -apple-system, sans-serif;
32
+ }
33
+ .bonsai-searchbar-wrapper {
34
+ position: relative;
35
+ }
36
+ .bonsai-searchbar {
37
+ display: flex;
38
+ align-items: center;
39
+ min-height: 56px;
40
+ position: relative;
41
+ z-index: 2;
42
+ background-color: var(--bonsai-input-bg);
43
+ border-radius: 8px;
44
+ transition: box-shadow 200ms ease-out;
45
+ }
46
+ .bonsai-searchbar.bonsai-searchbar-focused {
47
+ box-shadow: 0 0 0 2px var(--bonsai-brand-color);
48
+ }
49
+ .bonsai-searchbar-icon {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ padding-left: 16px;
54
+ padding-right: 8px;
55
+ color: var(--bonsai-muted-color);
56
+ transition: color 200ms ease-out;
57
+ }
58
+ .bonsai-searchbar-icon svg {
59
+ width: 20px;
60
+ height: 20px;
61
+ }
62
+ .bonsai-searchbar:hover .bonsai-searchbar-icon,
63
+ .bonsai-searchbar.bonsai-searchbar-focused .bonsai-searchbar-icon {
64
+ color: var(--bonsai-input-text-color, var(--bonsai-text-color));
65
+ }
66
+ .bonsai-searchbar-input {
67
+ flex: 1;
68
+ min-width: 0;
69
+ padding: 16px 16px 16px 0;
70
+ border: none;
71
+ background: transparent;
72
+ font-family: inherit;
73
+ font-size: 16px;
74
+ color: var(--bonsai-input-text-color, var(--bonsai-text-color));
75
+ outline: none;
76
+ }
77
+ .bonsai-searchbar-input::placeholder {
78
+ color: var(--bonsai-muted-color);
79
+ }
80
+ .bonsai-searchbar-actions {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ padding-right: 12px;
85
+ flex: 0 0 auto;
86
+ min-width: max-content;
87
+ }
88
+ .bonsai-searchbar-submit {
89
+ display: none;
90
+ align-items: center;
91
+ justify-content: center;
92
+ width: 32px;
93
+ height: 32px;
94
+ padding: 8px;
95
+ border: none;
96
+ border-radius: 8px;
97
+ background-color: var(--bonsai-brand-color);
98
+ cursor: pointer;
99
+ touch-action: manipulation;
100
+ flex-shrink: 0;
101
+ color: white;
102
+ animation: bonsai-searchbar-scale-in 0.2s ease-out forwards;
103
+ }
104
+ .bonsai-searchbar-submit.bonsai-searchbar-visible {
105
+ display: flex;
106
+ }
107
+ .bonsai-searchbar-submit svg {
108
+ width: 16px;
109
+ height: 16px;
110
+ }
111
+ .bonsai-searchbar-close {
112
+ display: none;
113
+ align-items: center;
114
+ justify-content: center;
115
+ width: 32px;
116
+ height: 32px;
117
+ padding: 8px;
118
+ border: none;
119
+ border-radius: 8px;
120
+ background: transparent;
121
+ cursor: pointer;
122
+ touch-action: manipulation;
123
+ flex-shrink: 0;
124
+ color: var(--bonsai-muted-color);
125
+ transition: color 200ms ease-out;
126
+ }
127
+ .bonsai-searchbar-close.bonsai-searchbar-visible {
128
+ display: flex;
129
+ }
130
+ .bonsai-searchbar-close:hover {
131
+ color: var(--bonsai-text-color);
132
+ }
133
+ .bonsai-searchbar-close svg {
134
+ width: 16px;
135
+ height: 16px;
136
+ }
137
+ .bonsai-searchbar-suggestions {
138
+ position: absolute;
139
+ top: calc(100% + 4px);
140
+ left: 0;
141
+ right: 0;
142
+ background-color: var(--bonsai-input-bg);
143
+ border-radius: 8px;
144
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
145
+ overflow: hidden;
146
+ z-index: 10;
147
+ display: none;
148
+ animation: bonsai-searchbar-fade-in 0.2s ease-out forwards;
149
+ }
150
+ .bonsai-searchbar-suggestions.bonsai-searchbar-visible {
151
+ display: block;
152
+ }
153
+ .bonsai-searchbar-suggestion {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ width: 100%;
158
+ padding: 8px 16px;
159
+ border: none;
160
+ font-family: inherit;
161
+ font-size: 14px;
162
+ color: var(--bonsai-suggestions-text-color, var(--bonsai-text-color));
163
+ text-align: left;
164
+ background-color: var(--bonsai-input-bg);
165
+ cursor: pointer;
166
+ transition: background-color 150ms ease-out;
167
+ }
168
+ .bonsai-searchbar-suggestion:hover {
169
+ background-color: var(--bonsai-suggestions-hover-bg, rgba(0, 0, 0, 0.04));
170
+ }
171
+ .bonsai-searchbar-suggestion-arrow {
172
+ color: var(--bonsai-muted-color);
173
+ opacity: 0;
174
+ visibility: hidden;
175
+ transition: opacity 150ms ease-out, visibility 0s linear 150ms;
176
+ }
177
+ .bonsai-searchbar-suggestion:hover .bonsai-searchbar-suggestion-arrow {
178
+ opacity: 1;
179
+ visibility: visible;
180
+ transition-delay: 0s;
181
+ }
182
+ .bonsai-searchbar-suggestion-arrow svg {
183
+ width: 16px;
184
+ height: 16px;
185
+ }
186
+ .bonsai-searchbar-powered {
187
+ position: relative;
188
+ z-index: 1;
189
+ height: 24px;
190
+ margin-top: 4px;
191
+ text-align: right;
192
+ font-family: ui-monospace, monospace;
193
+ font-size: 12px;
194
+ color: var(--bonsai-muted-color);
195
+ }
196
+ .bonsai-searchbar-powered-link {
197
+ color: var(--bonsai-brand-color);
198
+ text-decoration: none;
199
+ display: inline-flex;
200
+ align-items: center;
201
+ gap: 0;
202
+ transition: gap 0.2s ease-out;
203
+ }
204
+ .bonsai-searchbar-powered-link:hover {
205
+ gap: 4px;
206
+ }
207
+ .bonsai-searchbar-powered-dot {
208
+ display: inline-block;
209
+ height: 8px;
210
+ width: 0;
211
+ border-radius: 50%;
212
+ background-color: var(--bonsai-brand-color);
213
+ transform: scale(0);
214
+ transition: transform 0.2s ease-out, width 0.2s ease-out;
215
+ }
216
+ .bonsai-searchbar-powered-link:hover .bonsai-searchbar-powered-dot {
217
+ transform: scale(1);
218
+ width: 8px;
219
+ }
220
+ `;
221
+ var BonsaiSearchBar = class {
222
+ config;
223
+ state = "default";
224
+ container;
225
+ searchBar = null;
226
+ searchInput = null;
227
+ submitBtn = null;
228
+ suggestionsDropdown = null;
229
+ query = "";
230
+ stylesInjected = false;
231
+ constructor(config) {
232
+ this.container = config.container;
233
+ const inputBg = config.inputBg || config.inputBackground || "#f5f5f5";
234
+ this.config = {
235
+ container: config.container,
236
+ searchPath: config.searchPath || "/ai-search",
237
+ placeholder: config.placeholder || "Describe what you're looking for...",
238
+ suggestions: config.suggestions || [],
239
+ brandColor: config.brandColor || "#0A5B3B",
240
+ textColor: config.textColor || "#303030",
241
+ suggestionsTextColor: config.suggestionsTextColor || "",
242
+ inputTextColor: config.inputTextColor || "",
243
+ mutedColor: config.mutedColor || "#9CA3AF",
244
+ inputBg,
245
+ inputBackground: inputBg,
246
+ inputOpacity: config.inputOpacity || "1",
247
+ suggestionsHoverBg: config.suggestionsHoverBg || "",
248
+ theme: config.theme || "light",
249
+ closeButton: config.closeButton || false,
250
+ onClose: config.onClose || (() => {}),
251
+ classAdditions: config.classAdditions || {}
252
+ };
253
+ this.validateConfig();
254
+ this.injectStyles();
255
+ this.render();
256
+ this.attachEventListeners();
257
+ }
258
+ /** Get the SDK version */
259
+ static get version() {
260
+ return VERSION;
261
+ }
262
+ /** Validate configuration */
263
+ validateConfig() {
264
+ if (!this.config.container) throw new Error("BonsaiSearchBar: container element is required");
265
+ }
266
+ /** Build class name string with any configured additional classes */
267
+ buildClassName(baseClass) {
268
+ const additions = this.config.classAdditions[baseClass];
269
+ if (additions && additions.length > 0) return `${baseClass} ${additions.join(" ")}`;
270
+ return baseClass;
271
+ }
272
+ /** Apply a shadow part name for external styling */
273
+ applyPart(el, part) {
274
+ el.setAttribute("part", part);
275
+ }
276
+ /** Inject CSS styles into the document */
277
+ injectStyles() {
278
+ if (this.stylesInjected) return;
279
+ if (document.getElementById("bonsai-searchbar-styles")) {
280
+ this.stylesInjected = true;
281
+ return;
282
+ }
283
+ const styleElement = document.createElement("style");
284
+ styleElement.id = "bonsai-searchbar-styles";
285
+ styleElement.textContent = SEARCHBAR_STYLES;
286
+ document.head.appendChild(styleElement);
287
+ this.stylesInjected = true;
288
+ }
289
+ /** Render the search bar UI */
290
+ render() {
291
+ this.container.innerHTML = "";
292
+ this.container.classList.add("bonsai-searchbar-container");
293
+ this.applyPart(this.container, "searchbar-container");
294
+ this.container.setAttribute("data-theme", this.config.theme);
295
+ this.container.style.setProperty("--bonsai-brand-color", this.config.brandColor);
296
+ this.container.style.setProperty("--bonsai-text-color", this.config.textColor);
297
+ this.container.style.setProperty("--bonsai-muted-color", this.config.mutedColor);
298
+ this.container.style.setProperty("--bonsai-input-bg", this.config.inputBg);
299
+ if (this.config.suggestionsTextColor) this.container.style.setProperty("--bonsai-suggestions-text-color", this.config.suggestionsTextColor);
300
+ if (this.config.inputTextColor) this.container.style.setProperty("--bonsai-input-text-color", this.config.inputTextColor);
301
+ if (this.config.suggestionsHoverBg) this.container.style.setProperty("--bonsai-suggestions-hover-bg", this.config.suggestionsHoverBg);
302
+ const wrapper = document.createElement("div");
303
+ wrapper.className = this.buildClassName("bonsai-searchbar-wrapper");
304
+ this.applyPart(wrapper, "searchbar-wrapper");
305
+ this.searchBar = document.createElement("div");
306
+ this.searchBar.className = this.buildClassName("bonsai-searchbar");
307
+ this.applyPart(this.searchBar, "searchbar");
308
+ this.searchBar.style.opacity = this.config.inputOpacity;
309
+ const searchIcon = document.createElement("div");
310
+ searchIcon.className = this.buildClassName("bonsai-searchbar-icon");
311
+ this.applyPart(searchIcon, "searchbar-icon");
312
+ searchIcon.innerHTML = ICONS.search;
313
+ this.searchBar.appendChild(searchIcon);
314
+ this.searchInput = document.createElement("input");
315
+ this.searchInput.type = "text";
316
+ this.searchInput.className = this.buildClassName("bonsai-searchbar-input");
317
+ this.applyPart(this.searchInput, "searchbar-input");
318
+ this.searchInput.placeholder = this.config.placeholder;
319
+ this.searchInput.setAttribute("aria-label", "Search input");
320
+ this.searchBar.appendChild(this.searchInput);
321
+ const actionsContainer = document.createElement("div");
322
+ actionsContainer.className = this.buildClassName("bonsai-searchbar-actions");
323
+ this.applyPart(actionsContainer, "searchbar-actions");
324
+ this.submitBtn = document.createElement("button");
325
+ this.submitBtn.type = "button";
326
+ this.submitBtn.className = this.buildClassName("bonsai-searchbar-submit");
327
+ this.applyPart(this.submitBtn, "searchbar-submit");
328
+ this.submitBtn.setAttribute("aria-label", "Submit search");
329
+ this.submitBtn.innerHTML = ICONS.arrowRight;
330
+ actionsContainer.appendChild(this.submitBtn);
331
+ const closeBtn = document.createElement("button");
332
+ closeBtn.type = "button";
333
+ closeBtn.className = this.buildClassName("bonsai-searchbar-close");
334
+ if (this.config.closeButton) closeBtn.classList.add("bonsai-searchbar-visible");
335
+ this.applyPart(closeBtn, "searchbar-close");
336
+ closeBtn.setAttribute("aria-label", "Close search");
337
+ closeBtn.innerHTML = ICONS.close;
338
+ closeBtn.addEventListener("click", () => this.config.onClose());
339
+ actionsContainer.appendChild(closeBtn);
340
+ this.searchBar.appendChild(actionsContainer);
341
+ wrapper.appendChild(this.searchBar);
342
+ if (this.config.suggestions.length > 0) {
343
+ this.suggestionsDropdown = document.createElement("div");
344
+ this.suggestionsDropdown.className = this.buildClassName("bonsai-searchbar-suggestions");
345
+ this.applyPart(this.suggestionsDropdown, "searchbar-suggestions");
346
+ this.suggestionsDropdown.setAttribute("role", "listbox");
347
+ this.renderSuggestions();
348
+ wrapper.appendChild(this.suggestionsDropdown);
349
+ }
350
+ this.container.appendChild(wrapper);
351
+ const poweredBy = document.createElement("div");
352
+ poweredBy.className = this.buildClassName("bonsai-searchbar-powered");
353
+ poweredBy.innerHTML = `powered by <a href="https://www.hibonsai.com/" target="_blank" rel="noopener" class="bonsai-searchbar-powered-link v${VERSION}">Bonsai<span class="bonsai-searchbar-powered-dot"></span></a>`;
354
+ this.applyPart(poweredBy, "searchbar-powered");
355
+ const poweredLink = poweredBy.querySelector(".bonsai-searchbar-powered-link");
356
+ if (poweredLink) this.applyPart(poweredLink, "searchbar-powered-link");
357
+ const poweredDot = poweredBy.querySelector(".bonsai-searchbar-powered-dot");
358
+ if (poweredDot) this.applyPart(poweredDot, "searchbar-powered-dot");
359
+ this.container.appendChild(poweredBy);
360
+ }
361
+ /** Render suggestions in the dropdown */
362
+ renderSuggestions() {
363
+ if (!this.suggestionsDropdown) return;
364
+ this.suggestionsDropdown.innerHTML = "";
365
+ (this.query ? this.config.suggestions.filter((s) => s.toLowerCase().includes(this.query.toLowerCase())).slice(0, 5) : this.config.suggestions.slice(0, 5)).forEach((suggestion) => {
366
+ const item = document.createElement("button");
367
+ item.type = "button";
368
+ item.className = this.buildClassName("bonsai-searchbar-suggestion");
369
+ this.applyPart(item, "searchbar-suggestion");
370
+ item.setAttribute("role", "option");
371
+ const text = document.createElement("span");
372
+ text.textContent = suggestion;
373
+ this.applyPart(text, "searchbar-suggestion-text");
374
+ item.appendChild(text);
375
+ const arrow = document.createElement("div");
376
+ arrow.className = this.buildClassName("bonsai-searchbar-suggestion-arrow");
377
+ this.applyPart(arrow, "searchbar-suggestion-arrow");
378
+ arrow.innerHTML = ICONS.arrowRight;
379
+ item.appendChild(arrow);
380
+ item.addEventListener("click", () => this.handleSuggestionClick(suggestion));
381
+ this.suggestionsDropdown.appendChild(item);
382
+ });
383
+ }
384
+ /** Attach event listeners */
385
+ attachEventListeners() {
386
+ if (!this.searchInput) return;
387
+ this.searchInput.addEventListener("focus", () => this.handleFocus());
388
+ this.searchInput.addEventListener("blur", () => this.handleBlur());
389
+ this.searchInput.addEventListener("input", (e) => this.handleInput(e));
390
+ this.searchInput.addEventListener("keydown", (e) => this.handleKeyDown(e));
391
+ this.submitBtn?.addEventListener("click", () => this.handleSubmit());
392
+ }
393
+ /** Handle input focus */
394
+ handleFocus() {
395
+ if (this.state === "default") this.setState("focused");
396
+ this.searchBar?.classList.add("bonsai-searchbar-focused");
397
+ if (this.suggestionsDropdown) {
398
+ this.renderSuggestions();
399
+ this.suggestionsDropdown.classList.add("bonsai-searchbar-visible");
400
+ }
401
+ }
402
+ /** Handle input blur */
403
+ handleBlur() {
404
+ this.searchBar?.classList.remove("bonsai-searchbar-focused");
405
+ setTimeout(() => {
406
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
407
+ }, 200);
408
+ }
409
+ /** Handle input change */
410
+ handleInput(e) {
411
+ this.query = e.target.value;
412
+ if (this.query) {
413
+ this.setState("filled");
414
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
415
+ } else {
416
+ this.setState("focused");
417
+ if (this.suggestionsDropdown) {
418
+ this.renderSuggestions();
419
+ this.suggestionsDropdown.classList.add("bonsai-searchbar-visible");
420
+ }
421
+ }
422
+ }
423
+ /** Handle key down */
424
+ handleKeyDown(e) {
425
+ if (e.key === "Enter" && this.query) {
426
+ e.preventDefault();
427
+ this.handleSubmit();
428
+ }
429
+ }
430
+ /** Handle suggestion click */
431
+ handleSuggestionClick(suggestion) {
432
+ this.query = suggestion;
433
+ if (this.searchInput) this.searchInput.value = suggestion;
434
+ this.suggestionsDropdown?.classList.remove("bonsai-searchbar-visible");
435
+ this.handleSubmit();
436
+ }
437
+ /** Handle search submit - redirect to search page */
438
+ handleSubmit(searchQuery) {
439
+ const queryToSearch = searchQuery ?? this.query;
440
+ if (!queryToSearch) return;
441
+ try {
442
+ localStorage.setItem("bonsai-search-query", queryToSearch);
443
+ } catch {}
444
+ const url = `${this.config.searchPath}?q=${encodeURIComponent(queryToSearch)}`;
445
+ window.location.href = url;
446
+ }
447
+ /** Set the current state and update UI */
448
+ setState(newState) {
449
+ this.state = newState;
450
+ this.updateSubmitButton();
451
+ }
452
+ /** Update submit button visibility based on state */
453
+ updateSubmitButton() {
454
+ if (!this.submitBtn) return;
455
+ if (this.state === "filled") this.submitBtn.classList.add("bonsai-searchbar-visible");
456
+ else this.submitBtn.classList.remove("bonsai-searchbar-visible");
457
+ }
458
+ /** Set the search query programmatically */
459
+ setQuery(query) {
460
+ this.query = query;
461
+ if (this.searchInput) this.searchInput.value = query;
462
+ if (query) this.setState("filled");
463
+ else this.setState("default");
464
+ }
465
+ /** Submit the search programmatically */
466
+ submit() {
467
+ this.handleSubmit();
468
+ }
469
+ /** Clear the search input */
470
+ clear() {
471
+ this.query = "";
472
+ if (this.searchInput) this.searchInput.value = "";
473
+ this.setState("default");
474
+ }
475
+ /** Destroy the instance and clean up */
476
+ destroy() {
477
+ this.container.innerHTML = "";
478
+ }
479
+ };
480
+ if (typeof window !== "undefined") window.BonsaiSearchBar = BonsaiSearchBar;
481
+
482
+ //#endregion
483
+ exports.BonsaiSearchBar = BonsaiSearchBar;
484
+ exports.SEARCHBAR_STYLES = SEARCHBAR_STYLES;
485
+ return exports;
486
+ })({});