@huskel/sdk 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -1,96 +1,96 @@
1
- # @huskel/sdk
2
-
3
- AI-powered vector search SDK. You own your data — pass it in, we handle the rest.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @huskel/sdk
9
- ```
10
-
11
- ## Setup
12
-
13
- ```tsx
14
- // app/layout.tsx (Next.js) or _app.tsx
15
- 'use client';
16
- import { useHuskel } from '@huskel/sdk';
17
-
18
- export default function RootLayout({ children }) {
19
- useHuskel({
20
- siteId: 'your-site-id',
21
- apiUrl: 'https://your-huskel-backend.com',
22
- apiToken: 'your-api-token',
23
- });
24
- return <html><body>{children}</body></html>;
25
- }
26
- ```
27
-
28
- ## Ingest products (you pass your own data)
29
-
30
- ```tsx
31
- import { useIngest } from '@huskel/sdk';
32
-
33
- // Single product page
34
- export function ProductPage({ product }) {
35
- const { ingest } = useIngest();
36
-
37
- useEffect(() => {
38
- ingest({
39
- name: product.title,
40
- price: product.price,
41
- url: window.location.href,
42
- images: product.images,
43
- category: product.category,
44
- currency: 'KES',
45
- });
46
- }, [product.id]);
47
- }
48
-
49
- // Listing / category page
50
- export function ProductGrid({ products }) {
51
- const { ingestBatch } = useIngest();
52
-
53
- useEffect(() => {
54
- ingestBatch(products.map(p => ({
55
- name: p.title,
56
- price: p.price,
57
- url: `/products/${p.slug}`,
58
- images: [p.thumbnail],
59
- currency: 'KES',
60
- })));
61
- }, [products]);
62
- }
63
- ```
64
-
65
- ## Search
66
-
67
- ```tsx
68
- import { SearchBar } from '@huskel/sdk';
69
-
70
- export function Header() {
71
- return (
72
- <SearchBar
73
- onSelect={(result) => router.push(result.product.url)}
74
- />
75
- );
76
- }
77
- ```
78
-
79
- ```tsx
80
- // Headless
81
- import { useSearch } from '@huskel/sdk';
82
-
83
- const { results, loading, search } = useSearch();
84
- <input onChange={e => search(e.target.value)} />
85
- ```
86
-
87
- ## Sparkle (similar products)
88
-
89
- ```tsx
90
- import { Sparkle } from '@huskel/sdk';
91
-
92
- <Sparkle
93
- productName={product.name}
94
- onResult={(similar) => setSimilar(similar)}
95
- />
96
- ```
1
+ # @huskel/sdk
2
+
3
+ AI-powered vector search SDK. You own your data — pass it in, we handle the rest.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @huskel/sdk
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```tsx
14
+ // app/layout.tsx (Next.js) or _app.tsx
15
+ 'use client';
16
+ import { useHuskel } from '@huskel/sdk';
17
+
18
+ export default function RootLayout({ children }) {
19
+ useHuskel({
20
+ siteId: 'your-site-id',
21
+ apiUrl: 'https://your-huskel-backend.com',
22
+ apiToken: 'your-api-token',
23
+ });
24
+ return <html><body>{children}</body></html>;
25
+ }
26
+ ```
27
+
28
+ ## Ingest products (you pass your own data)
29
+
30
+ ```tsx
31
+ import { useIngest } from '@huskel/sdk';
32
+
33
+ // Single product page
34
+ export function ProductPage({ product }) {
35
+ const { ingest } = useIngest();
36
+
37
+ useEffect(() => {
38
+ ingest({
39
+ name: product.title,
40
+ price: product.price,
41
+ url: window.location.href,
42
+ images: product.images,
43
+ category: product.category,
44
+ currency: 'KES',
45
+ });
46
+ }, [product.id]);
47
+ }
48
+
49
+ // Listing / category page
50
+ export function ProductGrid({ products }) {
51
+ const { ingestBatch } = useIngest();
52
+
53
+ useEffect(() => {
54
+ ingestBatch(products.map(p => ({
55
+ name: p.title,
56
+ price: p.price,
57
+ url: `/products/${p.slug}`,
58
+ images: [p.thumbnail],
59
+ currency: 'KES',
60
+ })));
61
+ }, [products]);
62
+ }
63
+ ```
64
+
65
+ ## Search
66
+
67
+ ```tsx
68
+ import { SearchBar } from '@huskel/sdk';
69
+
70
+ export function Header() {
71
+ return (
72
+ <SearchBar
73
+ onSelect={(result) => router.push(result.product.url)}
74
+ />
75
+ );
76
+ }
77
+ ```
78
+
79
+ ```tsx
80
+ // Headless
81
+ import { useSearch } from '@huskel/sdk';
82
+
83
+ const { results, loading, search } = useSearch();
84
+ <input onChange={e => search(e.target.value)} />
85
+ ```
86
+
87
+ ## Sparkle (similar products)
88
+
89
+ ```tsx
90
+ import { Sparkle } from '@huskel/sdk';
91
+
92
+ <Sparkle
93
+ productName={product.name}
94
+ onResult={(similar) => setSimilar(similar)}
95
+ />
96
+ ```
package/dist/index.d.mts CHANGED
@@ -22,10 +22,37 @@ interface Product {
22
22
  priceNumeric?: number;
23
23
  slug?: string;
24
24
  }
25
+ interface RawProductInput {
26
+ name?: string;
27
+ title?: string;
28
+ productName?: string;
29
+ price?: string | number;
30
+ priceNumeric?: number;
31
+ url?: string;
32
+ image?: string;
33
+ thumbnail?: string;
34
+ images?: string[];
35
+ slug?: string;
36
+ id?: string;
37
+ productId?: string;
38
+ brand?: string;
39
+ description?: string;
40
+ originalPrice?: string;
41
+ discount?: string;
42
+ currency?: string;
43
+ stock?: string;
44
+ availability?: string;
45
+ rating?: string;
46
+ reviewCount?: number;
47
+ category?: string;
48
+ subCategory?: string;
49
+ tags?: string[];
50
+ specs?: Record<string, string>;
51
+ }
25
52
  interface HuskelConfig {
26
- siteId: string;
27
- apiUrl: string;
28
- apiToken: string;
53
+ siteId?: string;
54
+ apiUrl?: string;
55
+ apiToken?: string;
29
56
  }
30
57
  interface SearchRequest {
31
58
  query: string;
@@ -64,11 +91,23 @@ declare class HuskelAPI {
64
91
 
65
92
  declare class HuskelClient {
66
93
  readonly api: HuskelAPI;
94
+ private ingestQueue;
95
+ private ingestTimer;
96
+ private ingestedUrls;
97
+ private onlineHandler;
67
98
  constructor(config: HuskelConfig);
99
+ destroy(): void;
100
+ queueIngest(rawProduct: RawProductInput): Promise<void>;
101
+ queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
102
+ private scheduleFlush;
103
+ private flushQueue;
68
104
  }
69
105
  declare function initHuskel(config: HuskelConfig): HuskelClient;
70
106
  declare function getHuskelClient(): HuskelClient;
71
107
 
108
+ /**
109
+ * @deprecated Use <HuskelProvider> instead to avoid SSR issues.
110
+ */
72
111
  declare function useHuskel(config: HuskelConfig): HuskelClient;
73
112
 
74
113
  interface UseSearchReturn {
@@ -81,8 +120,8 @@ interface UseSearchReturn {
81
120
  declare function useSearch(): UseSearchReturn;
82
121
 
83
122
  interface UseIngestReturn {
84
- ingest: (product: Product) => Promise<void>;
85
- ingestBatch: (products: Product[]) => Promise<void>;
123
+ ingest: (product: RawProductInput) => Promise<void>;
124
+ ingestBatch: (products: RawProductInput[]) => Promise<void>;
86
125
  loading: boolean;
87
126
  error: string | null;
88
127
  }
@@ -108,4 +147,9 @@ interface SparkleProps {
108
147
  }
109
148
  declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
110
149
 
111
- export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, type IngestResponse, type Product, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };
150
+ interface HuskelProviderProps extends HuskelConfig {
151
+ children: React.ReactNode;
152
+ }
153
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
154
+
155
+ export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };
package/dist/index.d.ts CHANGED
@@ -22,10 +22,37 @@ interface Product {
22
22
  priceNumeric?: number;
23
23
  slug?: string;
24
24
  }
25
+ interface RawProductInput {
26
+ name?: string;
27
+ title?: string;
28
+ productName?: string;
29
+ price?: string | number;
30
+ priceNumeric?: number;
31
+ url?: string;
32
+ image?: string;
33
+ thumbnail?: string;
34
+ images?: string[];
35
+ slug?: string;
36
+ id?: string;
37
+ productId?: string;
38
+ brand?: string;
39
+ description?: string;
40
+ originalPrice?: string;
41
+ discount?: string;
42
+ currency?: string;
43
+ stock?: string;
44
+ availability?: string;
45
+ rating?: string;
46
+ reviewCount?: number;
47
+ category?: string;
48
+ subCategory?: string;
49
+ tags?: string[];
50
+ specs?: Record<string, string>;
51
+ }
25
52
  interface HuskelConfig {
26
- siteId: string;
27
- apiUrl: string;
28
- apiToken: string;
53
+ siteId?: string;
54
+ apiUrl?: string;
55
+ apiToken?: string;
29
56
  }
30
57
  interface SearchRequest {
31
58
  query: string;
@@ -64,11 +91,23 @@ declare class HuskelAPI {
64
91
 
65
92
  declare class HuskelClient {
66
93
  readonly api: HuskelAPI;
94
+ private ingestQueue;
95
+ private ingestTimer;
96
+ private ingestedUrls;
97
+ private onlineHandler;
67
98
  constructor(config: HuskelConfig);
99
+ destroy(): void;
100
+ queueIngest(rawProduct: RawProductInput): Promise<void>;
101
+ queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
102
+ private scheduleFlush;
103
+ private flushQueue;
68
104
  }
69
105
  declare function initHuskel(config: HuskelConfig): HuskelClient;
70
106
  declare function getHuskelClient(): HuskelClient;
71
107
 
108
+ /**
109
+ * @deprecated Use <HuskelProvider> instead to avoid SSR issues.
110
+ */
72
111
  declare function useHuskel(config: HuskelConfig): HuskelClient;
73
112
 
74
113
  interface UseSearchReturn {
@@ -81,8 +120,8 @@ interface UseSearchReturn {
81
120
  declare function useSearch(): UseSearchReturn;
82
121
 
83
122
  interface UseIngestReturn {
84
- ingest: (product: Product) => Promise<void>;
85
- ingestBatch: (products: Product[]) => Promise<void>;
123
+ ingest: (product: RawProductInput) => Promise<void>;
124
+ ingestBatch: (products: RawProductInput[]) => Promise<void>;
86
125
  loading: boolean;
87
126
  error: string | null;
88
127
  }
@@ -108,4 +147,9 @@ interface SparkleProps {
108
147
  }
109
148
  declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
110
149
 
111
- export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, type IngestResponse, type Product, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };
150
+ interface HuskelProviderProps extends HuskelConfig {
151
+ children: React.ReactNode;
152
+ }
153
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
154
+
155
+ export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  HuskelAPI: () => HuskelAPI,
24
24
  HuskelClient: () => HuskelClient,
25
+ HuskelProvider: () => HuskelProvider,
25
26
  SearchBar: () => SearchBar,
26
27
  Sparkle: () => Sparkle,
27
28
  getHuskelClient: () => getHuskelClient,
@@ -105,9 +106,169 @@ var HuskelAPI = class {
105
106
  };
106
107
 
107
108
  // src/client.ts
109
+ function getEnvVar(key) {
110
+ if (typeof globalThis !== "undefined") {
111
+ const g = globalThis;
112
+ if (g.process && g.process.env) {
113
+ return g.process.env[key];
114
+ }
115
+ }
116
+ return void 0;
117
+ }
118
+ function mapRawProduct(input) {
119
+ var _a;
120
+ const name = input.name || input.title || input.productName || "";
121
+ let price = "";
122
+ let priceNumeric = void 0;
123
+ if (input.price !== void 0) {
124
+ if (typeof input.price === "number") {
125
+ priceNumeric = input.price;
126
+ price = String(input.price);
127
+ } else {
128
+ price = input.price;
129
+ const num = parseFloat(input.price.replace(/[^0-9.]/g, ""));
130
+ priceNumeric = isNaN(num) ? void 0 : num;
131
+ }
132
+ }
133
+ if (input.priceNumeric !== void 0) {
134
+ priceNumeric = input.priceNumeric;
135
+ }
136
+ let url = input.url || "";
137
+ if (!url && typeof window !== "undefined") {
138
+ url = window.location.href;
139
+ }
140
+ let slug = input.slug || input.id || input.productId || "";
141
+ if (!slug && url) {
142
+ slug = url.split("/").filter(Boolean).pop() || "";
143
+ }
144
+ if (!slug && name) {
145
+ slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
146
+ }
147
+ let images = [];
148
+ if (input.images) {
149
+ images = input.images;
150
+ } else if (input.image) {
151
+ images = [input.image];
152
+ } else if (input.thumbnail) {
153
+ images = [input.thumbnail];
154
+ }
155
+ if (!name) {
156
+ console.warn("[Huskel] Validation warning: Product name/title is missing. Skipping:", input);
157
+ return null;
158
+ }
159
+ if (!price) {
160
+ console.warn("[Huskel] Validation warning: Product price is missing. Skipping:", input);
161
+ return null;
162
+ }
163
+ if (!url) {
164
+ console.warn("[Huskel] Validation warning: Product URL is missing. Skipping:", input);
165
+ return null;
166
+ }
167
+ return {
168
+ name,
169
+ price,
170
+ url,
171
+ brand: input.brand,
172
+ description: input.description,
173
+ originalPrice: input.originalPrice,
174
+ discount: input.discount,
175
+ currency: (_a = input.currency) != null ? _a : "KES",
176
+ stock: input.stock,
177
+ availability: input.availability,
178
+ rating: input.rating,
179
+ reviewCount: input.reviewCount,
180
+ category: input.category,
181
+ subCategory: input.subCategory,
182
+ tags: input.tags,
183
+ images: images.length > 0 ? images : void 0,
184
+ specs: input.specs,
185
+ priceNumeric,
186
+ slug
187
+ };
188
+ }
108
189
  var HuskelClient = class {
109
190
  constructor(config) {
110
- this.api = new HuskelAPI(config.apiUrl, config.siteId, config.apiToken);
191
+ this.ingestQueue = [];
192
+ this.ingestTimer = null;
193
+ this.ingestedUrls = /* @__PURE__ */ new Set();
194
+ this.onlineHandler = null;
195
+ const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
196
+ const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
197
+ const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
198
+ if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
199
+ if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
200
+ if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
201
+ this.api = new HuskelAPI(apiUrl, siteId, apiToken);
202
+ instance = this;
203
+ if (typeof window !== "undefined") {
204
+ this.onlineHandler = () => {
205
+ console.log("[Huskel] Connectivity restored, flushing queued ingestions.");
206
+ this.flushQueue();
207
+ };
208
+ window.addEventListener("online", this.onlineHandler);
209
+ }
210
+ }
211
+ destroy() {
212
+ if (typeof window !== "undefined" && this.onlineHandler) {
213
+ window.removeEventListener("online", this.onlineHandler);
214
+ this.onlineHandler = null;
215
+ }
216
+ if (this.ingestTimer) {
217
+ clearTimeout(this.ingestTimer);
218
+ this.ingestTimer = null;
219
+ }
220
+ if (instance === this) instance = null;
221
+ }
222
+ async queueIngest(rawProduct) {
223
+ const product = mapRawProduct(rawProduct);
224
+ if (!product) return;
225
+ if (this.ingestedUrls.has(product.url)) {
226
+ return;
227
+ }
228
+ this.ingestedUrls.add(product.url);
229
+ this.ingestQueue.push(product);
230
+ this.scheduleFlush();
231
+ }
232
+ async queueIngestBatch(rawProducts) {
233
+ rawProducts.forEach((p) => {
234
+ const product = mapRawProduct(p);
235
+ if (!product) return;
236
+ if (this.ingestedUrls.has(product.url)) {
237
+ return;
238
+ }
239
+ this.ingestedUrls.add(product.url);
240
+ this.ingestQueue.push(product);
241
+ });
242
+ if (this.ingestQueue.length > 0) {
243
+ this.scheduleFlush();
244
+ }
245
+ }
246
+ scheduleFlush() {
247
+ if (this.ingestTimer) return;
248
+ this.ingestTimer = setTimeout(() => {
249
+ this.flushQueue();
250
+ }, 300);
251
+ }
252
+ async flushQueue() {
253
+ this.ingestTimer = null;
254
+ if (this.ingestQueue.length === 0) return;
255
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
256
+ console.warn("[Huskel] Browser offline. Postponing ingestion.");
257
+ return;
258
+ }
259
+ const batch = [...this.ingestQueue];
260
+ this.ingestQueue = [];
261
+ try {
262
+ await this.api.ingestBatch(batch);
263
+ } catch (e) {
264
+ if (e.status && e.status >= 400 && e.status < 500) {
265
+ console.error("[Huskel] Ingestion discarded due to client error:", e.message);
266
+ return;
267
+ }
268
+ console.warn("[Huskel] Ingestion failed. Re-queuing to retry.", e);
269
+ this.ingestQueue = [...batch, ...this.ingestQueue];
270
+ this.scheduleFlush();
271
+ }
111
272
  }
112
273
  };
113
274
  var instance = null;
@@ -116,7 +277,16 @@ function initHuskel(config) {
116
277
  return instance;
117
278
  }
118
279
  function getHuskelClient() {
119
- if (!instance) throw new Error("[Huskel] Call initHuskel() before using the client.");
280
+ if (!instance) {
281
+ const siteId = getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID");
282
+ const apiUrl = getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL");
283
+ const apiToken = getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN");
284
+ if (siteId && apiUrl && apiToken) {
285
+ instance = new HuskelClient({ siteId, apiUrl, apiToken });
286
+ } else {
287
+ throw new Error("[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.");
288
+ }
289
+ }
120
290
  return instance;
121
291
  }
122
292
 
@@ -125,19 +295,48 @@ var import_react = require("react");
125
295
  function useHuskel(config) {
126
296
  const clientRef = (0, import_react.useRef)(null);
127
297
  if (!clientRef.current) {
298
+ console.warn("[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.");
128
299
  clientRef.current = initHuskel(config);
129
300
  }
130
301
  return clientRef.current;
131
302
  }
132
303
 
133
304
  // src/hooks/useSearch.ts
305
+ var import_react3 = require("react");
306
+
307
+ // src/components/HuskelProvider.tsx
134
308
  var import_react2 = require("react");
309
+ var import_jsx_runtime = require("react/jsx-runtime");
310
+ var HuskelContext = (0, import_react2.createContext)(null);
311
+ function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
312
+ const clientRef = (0, import_react2.useRef)(null);
313
+ if (!clientRef.current) {
314
+ clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
315
+ }
316
+ (0, import_react2.useEffect)(() => {
317
+ return () => {
318
+ var _a;
319
+ (_a = clientRef.current) == null ? void 0 : _a.destroy();
320
+ };
321
+ }, []);
322
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HuskelContext.Provider, { value: clientRef.current, children });
323
+ }
324
+ function useHuskelContext() {
325
+ const context = (0, import_react2.useContext)(HuskelContext);
326
+ if (!context) {
327
+ return getHuskelClient();
328
+ }
329
+ return context;
330
+ }
331
+
332
+ // src/hooks/useSearch.ts
135
333
  function useSearch() {
136
- const [results, setResults] = (0, import_react2.useState)([]);
137
- const [loading, setLoading] = (0, import_react2.useState)(false);
138
- const [error, setError] = (0, import_react2.useState)(null);
139
- const abortRef = (0, import_react2.useRef)(null);
140
- const search = (0, import_react2.useCallback)(async (query, limit = 10) => {
334
+ const client = useHuskelContext();
335
+ const [results, setResults] = (0, import_react3.useState)([]);
336
+ const [loading, setLoading] = (0, import_react3.useState)(false);
337
+ const [error, setError] = (0, import_react3.useState)(null);
338
+ const abortRef = (0, import_react3.useRef)(null);
339
+ const search = (0, import_react3.useCallback)(async (query, limit = 10) => {
141
340
  var _a, _b, _c;
142
341
  if (!query.trim()) {
143
342
  setResults([]);
@@ -148,15 +347,15 @@ function useSearch() {
148
347
  setLoading(true);
149
348
  setError(null);
150
349
  try {
151
- const res = await getHuskelClient().api.search(query, limit);
350
+ const res = await client.api.search(query, limit);
152
351
  setResults((_b = res.results) != null ? _b : []);
153
352
  } catch (e) {
154
353
  setError((_c = e.message) != null ? _c : "Search failed");
155
354
  } finally {
156
355
  setLoading(false);
157
356
  }
158
- }, []);
159
- const clear = (0, import_react2.useCallback)(() => {
357
+ }, [client]);
358
+ const clear = (0, import_react3.useCallback)(() => {
160
359
  setResults([]);
161
360
  setError(null);
162
361
  }, []);
@@ -164,41 +363,42 @@ function useSearch() {
164
363
  }
165
364
 
166
365
  // src/hooks/useIngest.ts
167
- var import_react3 = require("react");
366
+ var import_react4 = require("react");
168
367
  function useIngest() {
169
- const [loading, setLoading] = (0, import_react3.useState)(false);
170
- const [error, setError] = (0, import_react3.useState)(null);
171
- const ingest = (0, import_react3.useCallback)(async (product) => {
368
+ const client = useHuskelContext();
369
+ const [loading, setLoading] = (0, import_react4.useState)(false);
370
+ const [error, setError] = (0, import_react4.useState)(null);
371
+ const ingest = (0, import_react4.useCallback)(async (product) => {
172
372
  var _a;
173
373
  setLoading(true);
174
374
  setError(null);
175
375
  try {
176
- await getHuskelClient().api.ingest(product);
376
+ await client.queueIngest(product);
177
377
  } catch (e) {
178
378
  setError((_a = e.message) != null ? _a : "Ingest failed");
179
379
  } finally {
180
380
  setLoading(false);
181
381
  }
182
- }, []);
183
- const ingestBatch = (0, import_react3.useCallback)(async (products) => {
382
+ }, [client]);
383
+ const ingestBatch = (0, import_react4.useCallback)(async (products) => {
184
384
  var _a;
185
385
  if (!products.length) return;
186
386
  setLoading(true);
187
387
  setError(null);
188
388
  try {
189
- await getHuskelClient().api.ingestBatch(products);
389
+ await client.queueIngestBatch(products);
190
390
  } catch (e) {
191
391
  setError((_a = e.message) != null ? _a : "Batch ingest failed");
192
392
  } finally {
193
393
  setLoading(false);
194
394
  }
195
- }, []);
395
+ }, [client]);
196
396
  return { ingest, ingestBatch, loading, error };
197
397
  }
198
398
 
199
399
  // src/components/SearchBar.tsx
200
- var import_react4 = require("react");
201
- var import_jsx_runtime = require("react/jsx-runtime");
400
+ var import_react5 = require("react");
401
+ var import_jsx_runtime2 = require("react/jsx-runtime");
202
402
  var S = `
203
403
  .hsk-wrap{position:relative;width:100%;font-family:inherit}
204
404
  .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}
@@ -221,12 +421,12 @@ function SearchBar({
221
421
  dropdownClassName,
222
422
  renderResult
223
423
  }) {
224
- const [query, setQuery] = (0, import_react4.useState)("");
225
- const [open, setOpen] = (0, import_react4.useState)(false);
424
+ const [query, setQuery] = (0, import_react5.useState)("");
425
+ const [open, setOpen] = (0, import_react5.useState)(false);
226
426
  const { results, loading, search, clear } = useSearch();
227
- const timer = (0, import_react4.useRef)();
228
- const wrap = (0, import_react4.useRef)(null);
229
- (0, import_react4.useEffect)(() => {
427
+ const timer = (0, import_react5.useRef)();
428
+ const wrap = (0, import_react5.useRef)(null);
429
+ (0, import_react5.useEffect)(() => {
230
430
  clearTimeout(timer.current);
231
431
  if (!query.trim()) {
232
432
  clear();
@@ -238,8 +438,8 @@ function SearchBar({
238
438
  setOpen(true);
239
439
  }, debounceMs);
240
440
  return () => clearTimeout(timer.current);
241
- }, [query]);
242
- (0, import_react4.useEffect)(() => {
441
+ }, [query, search, clear, limit, debounceMs]);
442
+ (0, import_react5.useEffect)(() => {
243
443
  const handler = (e) => {
244
444
  if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
245
445
  };
@@ -251,10 +451,10 @@ function SearchBar({
251
451
  setQuery(r.product.name);
252
452
  onSelect == null ? void 0 : onSelect(r);
253
453
  };
254
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
255
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: S }),
256
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
257
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
454
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: S }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
258
458
  "input",
259
459
  {
260
460
  className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
@@ -265,9 +465,9 @@ function SearchBar({
265
465
  onFocus: () => results.length && setOpen(true)
266
466
  }
267
467
  ),
268
- open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
269
- loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hsk-msg", children: "Searching\u2026" }),
270
- !loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-msg", children: [
468
+ open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
469
+ loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-msg", children: "Searching\u2026" }),
470
+ !loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-msg", children: [
271
471
  'No results for "',
272
472
  query,
273
473
  '"'
@@ -275,11 +475,11 @@ function SearchBar({
275
475
  results.map(
276
476
  (r) => {
277
477
  var _a, _b;
278
- return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
279
- ((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: r.product.images[0], alt: r.product.name }),
280
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
281
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hsk-item-name", children: r.product.name }),
282
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-item-price", children: [
478
+ return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
479
+ ((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: r.product.images[0], alt: r.product.name }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
481
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-item-name", children: r.product.name }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item-price", children: [
283
483
  (_b = r.product.currency) != null ? _b : "KES",
284
484
  " ",
285
485
  r.product.price
@@ -294,19 +494,20 @@ function SearchBar({
294
494
  }
295
495
 
296
496
  // src/components/Sparkle.tsx
297
- var import_react5 = require("react");
298
- var import_jsx_runtime2 = require("react/jsx-runtime");
497
+ var import_react6 = require("react");
498
+ var import_jsx_runtime3 = require("react/jsx-runtime");
299
499
  var S2 = `
300
500
  .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}
301
501
  .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
302
502
  .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
303
503
  `;
304
504
  function Sparkle({ productName, limit = 5, onResult, className }) {
305
- const [loading, setLoading] = (0, import_react5.useState)(false);
505
+ const client = useHuskelContext();
506
+ const [loading, setLoading] = (0, import_react6.useState)(false);
306
507
  const handleClick = async () => {
307
508
  setLoading(true);
308
509
  try {
309
- const res = await getHuskelClient().api.search(productName, limit);
510
+ const res = await client.api.search(productName, limit);
310
511
  onResult == null ? void 0 : onResult(res.results);
311
512
  } catch (e) {
312
513
  console.error("[Huskel Sparkle]", e);
@@ -314,9 +515,9 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
314
515
  setLoading(false);
315
516
  }
316
517
  };
317
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
318
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: S2 }),
319
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
518
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: S2 }),
520
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
320
521
  "\u2726 ",
321
522
  loading ? "Finding\u2026" : "Similar"
322
523
  ] })
@@ -326,6 +527,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
326
527
  0 && (module.exports = {
327
528
  HuskelAPI,
328
529
  HuskelClient,
530
+ HuskelProvider,
329
531
  SearchBar,
330
532
  Sparkle,
331
533
  getHuskelClient,
package/dist/index.mjs CHANGED
@@ -71,9 +71,169 @@ var HuskelAPI = class {
71
71
  };
72
72
 
73
73
  // src/client.ts
74
+ function getEnvVar(key) {
75
+ if (typeof globalThis !== "undefined") {
76
+ const g = globalThis;
77
+ if (g.process && g.process.env) {
78
+ return g.process.env[key];
79
+ }
80
+ }
81
+ return void 0;
82
+ }
83
+ function mapRawProduct(input) {
84
+ var _a;
85
+ const name = input.name || input.title || input.productName || "";
86
+ let price = "";
87
+ let priceNumeric = void 0;
88
+ if (input.price !== void 0) {
89
+ if (typeof input.price === "number") {
90
+ priceNumeric = input.price;
91
+ price = String(input.price);
92
+ } else {
93
+ price = input.price;
94
+ const num = parseFloat(input.price.replace(/[^0-9.]/g, ""));
95
+ priceNumeric = isNaN(num) ? void 0 : num;
96
+ }
97
+ }
98
+ if (input.priceNumeric !== void 0) {
99
+ priceNumeric = input.priceNumeric;
100
+ }
101
+ let url = input.url || "";
102
+ if (!url && typeof window !== "undefined") {
103
+ url = window.location.href;
104
+ }
105
+ let slug = input.slug || input.id || input.productId || "";
106
+ if (!slug && url) {
107
+ slug = url.split("/").filter(Boolean).pop() || "";
108
+ }
109
+ if (!slug && name) {
110
+ slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
111
+ }
112
+ let images = [];
113
+ if (input.images) {
114
+ images = input.images;
115
+ } else if (input.image) {
116
+ images = [input.image];
117
+ } else if (input.thumbnail) {
118
+ images = [input.thumbnail];
119
+ }
120
+ if (!name) {
121
+ console.warn("[Huskel] Validation warning: Product name/title is missing. Skipping:", input);
122
+ return null;
123
+ }
124
+ if (!price) {
125
+ console.warn("[Huskel] Validation warning: Product price is missing. Skipping:", input);
126
+ return null;
127
+ }
128
+ if (!url) {
129
+ console.warn("[Huskel] Validation warning: Product URL is missing. Skipping:", input);
130
+ return null;
131
+ }
132
+ return {
133
+ name,
134
+ price,
135
+ url,
136
+ brand: input.brand,
137
+ description: input.description,
138
+ originalPrice: input.originalPrice,
139
+ discount: input.discount,
140
+ currency: (_a = input.currency) != null ? _a : "KES",
141
+ stock: input.stock,
142
+ availability: input.availability,
143
+ rating: input.rating,
144
+ reviewCount: input.reviewCount,
145
+ category: input.category,
146
+ subCategory: input.subCategory,
147
+ tags: input.tags,
148
+ images: images.length > 0 ? images : void 0,
149
+ specs: input.specs,
150
+ priceNumeric,
151
+ slug
152
+ };
153
+ }
74
154
  var HuskelClient = class {
75
155
  constructor(config) {
76
- this.api = new HuskelAPI(config.apiUrl, config.siteId, config.apiToken);
156
+ this.ingestQueue = [];
157
+ this.ingestTimer = null;
158
+ this.ingestedUrls = /* @__PURE__ */ new Set();
159
+ this.onlineHandler = null;
160
+ const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
161
+ const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
162
+ const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
163
+ if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
164
+ if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
165
+ if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
166
+ this.api = new HuskelAPI(apiUrl, siteId, apiToken);
167
+ instance = this;
168
+ if (typeof window !== "undefined") {
169
+ this.onlineHandler = () => {
170
+ console.log("[Huskel] Connectivity restored, flushing queued ingestions.");
171
+ this.flushQueue();
172
+ };
173
+ window.addEventListener("online", this.onlineHandler);
174
+ }
175
+ }
176
+ destroy() {
177
+ if (typeof window !== "undefined" && this.onlineHandler) {
178
+ window.removeEventListener("online", this.onlineHandler);
179
+ this.onlineHandler = null;
180
+ }
181
+ if (this.ingestTimer) {
182
+ clearTimeout(this.ingestTimer);
183
+ this.ingestTimer = null;
184
+ }
185
+ if (instance === this) instance = null;
186
+ }
187
+ async queueIngest(rawProduct) {
188
+ const product = mapRawProduct(rawProduct);
189
+ if (!product) return;
190
+ if (this.ingestedUrls.has(product.url)) {
191
+ return;
192
+ }
193
+ this.ingestedUrls.add(product.url);
194
+ this.ingestQueue.push(product);
195
+ this.scheduleFlush();
196
+ }
197
+ async queueIngestBatch(rawProducts) {
198
+ rawProducts.forEach((p) => {
199
+ const product = mapRawProduct(p);
200
+ if (!product) return;
201
+ if (this.ingestedUrls.has(product.url)) {
202
+ return;
203
+ }
204
+ this.ingestedUrls.add(product.url);
205
+ this.ingestQueue.push(product);
206
+ });
207
+ if (this.ingestQueue.length > 0) {
208
+ this.scheduleFlush();
209
+ }
210
+ }
211
+ scheduleFlush() {
212
+ if (this.ingestTimer) return;
213
+ this.ingestTimer = setTimeout(() => {
214
+ this.flushQueue();
215
+ }, 300);
216
+ }
217
+ async flushQueue() {
218
+ this.ingestTimer = null;
219
+ if (this.ingestQueue.length === 0) return;
220
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
221
+ console.warn("[Huskel] Browser offline. Postponing ingestion.");
222
+ return;
223
+ }
224
+ const batch = [...this.ingestQueue];
225
+ this.ingestQueue = [];
226
+ try {
227
+ await this.api.ingestBatch(batch);
228
+ } catch (e) {
229
+ if (e.status && e.status >= 400 && e.status < 500) {
230
+ console.error("[Huskel] Ingestion discarded due to client error:", e.message);
231
+ return;
232
+ }
233
+ console.warn("[Huskel] Ingestion failed. Re-queuing to retry.", e);
234
+ this.ingestQueue = [...batch, ...this.ingestQueue];
235
+ this.scheduleFlush();
236
+ }
77
237
  }
78
238
  };
79
239
  var instance = null;
@@ -82,7 +242,16 @@ function initHuskel(config) {
82
242
  return instance;
83
243
  }
84
244
  function getHuskelClient() {
85
- if (!instance) throw new Error("[Huskel] Call initHuskel() before using the client.");
245
+ if (!instance) {
246
+ const siteId = getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID");
247
+ const apiUrl = getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL");
248
+ const apiToken = getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN");
249
+ if (siteId && apiUrl && apiToken) {
250
+ instance = new HuskelClient({ siteId, apiUrl, apiToken });
251
+ } else {
252
+ throw new Error("[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.");
253
+ }
254
+ }
86
255
  return instance;
87
256
  }
88
257
 
@@ -91,18 +260,47 @@ import { useRef } from "react";
91
260
  function useHuskel(config) {
92
261
  const clientRef = useRef(null);
93
262
  if (!clientRef.current) {
263
+ console.warn("[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.");
94
264
  clientRef.current = initHuskel(config);
95
265
  }
96
266
  return clientRef.current;
97
267
  }
98
268
 
99
269
  // src/hooks/useSearch.ts
100
- import { useState, useCallback, useRef as useRef2 } from "react";
270
+ import { useState, useCallback, useRef as useRef3 } from "react";
271
+
272
+ // src/components/HuskelProvider.tsx
273
+ import { createContext, useContext, useEffect, useRef as useRef2 } from "react";
274
+ import { jsx } from "react/jsx-runtime";
275
+ var HuskelContext = createContext(null);
276
+ function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
277
+ const clientRef = useRef2(null);
278
+ if (!clientRef.current) {
279
+ clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
280
+ }
281
+ useEffect(() => {
282
+ return () => {
283
+ var _a;
284
+ (_a = clientRef.current) == null ? void 0 : _a.destroy();
285
+ };
286
+ }, []);
287
+ return /* @__PURE__ */ jsx(HuskelContext.Provider, { value: clientRef.current, children });
288
+ }
289
+ function useHuskelContext() {
290
+ const context = useContext(HuskelContext);
291
+ if (!context) {
292
+ return getHuskelClient();
293
+ }
294
+ return context;
295
+ }
296
+
297
+ // src/hooks/useSearch.ts
101
298
  function useSearch() {
299
+ const client = useHuskelContext();
102
300
  const [results, setResults] = useState([]);
103
301
  const [loading, setLoading] = useState(false);
104
302
  const [error, setError] = useState(null);
105
- const abortRef = useRef2(null);
303
+ const abortRef = useRef3(null);
106
304
  const search = useCallback(async (query, limit = 10) => {
107
305
  var _a, _b, _c;
108
306
  if (!query.trim()) {
@@ -114,14 +312,14 @@ function useSearch() {
114
312
  setLoading(true);
115
313
  setError(null);
116
314
  try {
117
- const res = await getHuskelClient().api.search(query, limit);
315
+ const res = await client.api.search(query, limit);
118
316
  setResults((_b = res.results) != null ? _b : []);
119
317
  } catch (e) {
120
318
  setError((_c = e.message) != null ? _c : "Search failed");
121
319
  } finally {
122
320
  setLoading(false);
123
321
  }
124
- }, []);
322
+ }, [client]);
125
323
  const clear = useCallback(() => {
126
324
  setResults([]);
127
325
  setError(null);
@@ -132,6 +330,7 @@ function useSearch() {
132
330
  // src/hooks/useIngest.ts
133
331
  import { useCallback as useCallback2, useState as useState2 } from "react";
134
332
  function useIngest() {
333
+ const client = useHuskelContext();
135
334
  const [loading, setLoading] = useState2(false);
136
335
  const [error, setError] = useState2(null);
137
336
  const ingest = useCallback2(async (product) => {
@@ -139,32 +338,32 @@ function useIngest() {
139
338
  setLoading(true);
140
339
  setError(null);
141
340
  try {
142
- await getHuskelClient().api.ingest(product);
341
+ await client.queueIngest(product);
143
342
  } catch (e) {
144
343
  setError((_a = e.message) != null ? _a : "Ingest failed");
145
344
  } finally {
146
345
  setLoading(false);
147
346
  }
148
- }, []);
347
+ }, [client]);
149
348
  const ingestBatch = useCallback2(async (products) => {
150
349
  var _a;
151
350
  if (!products.length) return;
152
351
  setLoading(true);
153
352
  setError(null);
154
353
  try {
155
- await getHuskelClient().api.ingestBatch(products);
354
+ await client.queueIngestBatch(products);
156
355
  } catch (e) {
157
356
  setError((_a = e.message) != null ? _a : "Batch ingest failed");
158
357
  } finally {
159
358
  setLoading(false);
160
359
  }
161
- }, []);
360
+ }, [client]);
162
361
  return { ingest, ingestBatch, loading, error };
163
362
  }
164
363
 
165
364
  // src/components/SearchBar.tsx
166
- import { useState as useState3, useEffect as useEffect2, useRef as useRef3 } from "react";
167
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
365
+ import { useState as useState3, useEffect as useEffect2, useRef as useRef4 } from "react";
366
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
168
367
  var S = `
169
368
  .hsk-wrap{position:relative;width:100%;font-family:inherit}
170
369
  .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}
@@ -190,8 +389,8 @@ function SearchBar({
190
389
  const [query, setQuery] = useState3("");
191
390
  const [open, setOpen] = useState3(false);
192
391
  const { results, loading, search, clear } = useSearch();
193
- const timer = useRef3();
194
- const wrap = useRef3(null);
392
+ const timer = useRef4();
393
+ const wrap = useRef4(null);
195
394
  useEffect2(() => {
196
395
  clearTimeout(timer.current);
197
396
  if (!query.trim()) {
@@ -204,7 +403,7 @@ function SearchBar({
204
403
  setOpen(true);
205
404
  }, debounceMs);
206
405
  return () => clearTimeout(timer.current);
207
- }, [query]);
406
+ }, [query, search, clear, limit, debounceMs]);
208
407
  useEffect2(() => {
209
408
  const handler = (e) => {
210
409
  if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
@@ -218,9 +417,9 @@ function SearchBar({
218
417
  onSelect == null ? void 0 : onSelect(r);
219
418
  };
220
419
  return /* @__PURE__ */ jsxs(Fragment, { children: [
221
- /* @__PURE__ */ jsx("style", { children: S }),
420
+ /* @__PURE__ */ jsx2("style", { children: S }),
222
421
  /* @__PURE__ */ jsxs("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
223
- /* @__PURE__ */ jsx(
422
+ /* @__PURE__ */ jsx2(
224
423
  "input",
225
424
  {
226
425
  className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
@@ -232,7 +431,7 @@ function SearchBar({
232
431
  }
233
432
  ),
234
433
  open && /* @__PURE__ */ jsxs("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
235
- loading && /* @__PURE__ */ jsx("div", { className: "hsk-msg", children: "Searching\u2026" }),
434
+ loading && /* @__PURE__ */ jsx2("div", { className: "hsk-msg", children: "Searching\u2026" }),
236
435
  !loading && results.length === 0 && /* @__PURE__ */ jsxs("div", { className: "hsk-msg", children: [
237
436
  'No results for "',
238
437
  query,
@@ -241,10 +440,10 @@ function SearchBar({
241
440
  results.map(
242
441
  (r) => {
243
442
  var _a, _b;
244
- return renderResult ? /* @__PURE__ */ jsx("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ jsxs("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
245
- ((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ jsx("img", { src: r.product.images[0], alt: r.product.name }),
443
+ return renderResult ? /* @__PURE__ */ jsx2("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ jsxs("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
444
+ ((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ jsx2("img", { src: r.product.images[0], alt: r.product.name }),
246
445
  /* @__PURE__ */ jsxs("div", { children: [
247
- /* @__PURE__ */ jsx("div", { className: "hsk-item-name", children: r.product.name }),
446
+ /* @__PURE__ */ jsx2("div", { className: "hsk-item-name", children: r.product.name }),
248
447
  /* @__PURE__ */ jsxs("div", { className: "hsk-item-price", children: [
249
448
  (_b = r.product.currency) != null ? _b : "KES",
250
449
  " ",
@@ -261,18 +460,19 @@ function SearchBar({
261
460
 
262
461
  // src/components/Sparkle.tsx
263
462
  import { useState as useState4 } from "react";
264
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
463
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
265
464
  var S2 = `
266
465
  .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}
267
466
  .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
268
467
  .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
269
468
  `;
270
469
  function Sparkle({ productName, limit = 5, onResult, className }) {
470
+ const client = useHuskelContext();
271
471
  const [loading, setLoading] = useState4(false);
272
472
  const handleClick = async () => {
273
473
  setLoading(true);
274
474
  try {
275
- const res = await getHuskelClient().api.search(productName, limit);
475
+ const res = await client.api.search(productName, limit);
276
476
  onResult == null ? void 0 : onResult(res.results);
277
477
  } catch (e) {
278
478
  console.error("[Huskel Sparkle]", e);
@@ -281,7 +481,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
281
481
  }
282
482
  };
283
483
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
284
- /* @__PURE__ */ jsx2("style", { children: S2 }),
484
+ /* @__PURE__ */ jsx3("style", { children: S2 }),
285
485
  /* @__PURE__ */ jsxs2("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
286
486
  "\u2726 ",
287
487
  loading ? "Finding\u2026" : "Similar"
@@ -291,6 +491,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
291
491
  export {
292
492
  HuskelAPI,
293
493
  HuskelClient,
494
+ HuskelProvider,
294
495
  SearchBar,
295
496
  Sparkle,
296
497
  getHuskelClient,
package/package.json CHANGED
@@ -1,26 +1,32 @@
1
1
  {
2
2
  "name": "@huskel/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Huskel AI-powered search SDK for SPAs",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
7
7
  "types": "dist/index.d.ts",
8
- "files": ["dist"],
9
- "scripts": {
10
- "build": "tsup src/index.ts --format cjs,esm --dts",
11
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
12
- },
8
+ "files": [
9
+ "dist"
10
+ ],
13
11
  "peerDependencies": {
14
12
  "react": ">=17",
15
13
  "react-dom": ">=17"
16
14
  },
17
15
  "peerDependenciesMeta": {
18
- "react": { "optional": true },
19
- "react-dom": { "optional": true }
16
+ "react": {
17
+ "optional": true
18
+ },
19
+ "react-dom": {
20
+ "optional": true
21
+ }
20
22
  },
21
23
  "devDependencies": {
22
24
  "@types/react": "^18.0.0",
23
25
  "tsup": "^8.0.0",
24
26
  "typescript": "^5.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup src/index.ts --format cjs,esm --dts",
30
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
25
31
  }
26
- }
32
+ }