@huskel/sdk 0.1.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/dist/index.js CHANGED
@@ -2,21 +2,7 @@
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
8
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
- var __spreadValues = (a, b) => {
10
- for (var prop in b || (b = {}))
11
- if (__hasOwnProp.call(b, prop))
12
- __defNormalProp(a, prop, b[prop]);
13
- if (__getOwnPropSymbols)
14
- for (var prop of __getOwnPropSymbols(b)) {
15
- if (__propIsEnum.call(b, prop))
16
- __defNormalProp(a, prop, b[prop]);
17
- }
18
- return a;
19
- };
20
6
  var __export = (target, all) => {
21
7
  for (var name in all)
22
8
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -36,175 +22,253 @@ var index_exports = {};
36
22
  __export(index_exports, {
37
23
  HuskelAPI: () => HuskelAPI,
38
24
  HuskelClient: () => HuskelClient,
25
+ HuskelProvider: () => HuskelProvider,
39
26
  SearchBar: () => SearchBar,
40
27
  Sparkle: () => Sparkle,
41
28
  getHuskelClient: () => getHuskelClient,
42
29
  initHuskel: () => initHuskel,
43
30
  useHuskel: () => useHuskel,
31
+ useIngest: () => useIngest,
44
32
  useSearch: () => useSearch
45
33
  });
46
34
  module.exports = __toCommonJS(index_exports);
47
35
 
48
36
  // src/api.ts
37
+ var MAX_RETRIES = 3;
38
+ var RETRY_DELAYS = [500, 1e3, 2e3];
39
+ function log(level, msg, data) {
40
+ const prefix = "[Huskel]";
41
+ if (level === "error") console.error(prefix, msg, data != null ? data : "");
42
+ else if (level === "warn") console.warn(prefix, msg, data != null ? data : "");
43
+ else console.log(prefix, msg, data != null ? data : "");
44
+ }
45
+ async function sleep(ms) {
46
+ return new Promise((r) => setTimeout(r, ms));
47
+ }
49
48
  var HuskelAPI = class {
50
- constructor(apiUrl, siteId) {
49
+ constructor(apiUrl, siteId, apiToken) {
51
50
  this.apiUrl = apiUrl;
52
51
  this.siteId = siteId;
52
+ this.apiToken = apiToken;
53
53
  }
54
- async post(path, body) {
55
- const res = await fetch(`${this.apiUrl}${path}`, {
56
- method: "POST",
57
- headers: { "Content-Type": "application/json" },
58
- body: JSON.stringify(body)
59
- });
60
- if (!res.ok) {
61
- const err = await res.text();
62
- throw new Error(`Huskel API ${path} [${res.status}]: ${err}`);
54
+ async post(path, body, attempt = 0) {
55
+ const url = `${this.apiUrl}${path}`;
56
+ try {
57
+ const res = await fetch(url, {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ "X-Huskel-Token": this.apiToken,
62
+ "X-Huskel-Site": this.siteId
63
+ },
64
+ body: JSON.stringify(body)
65
+ });
66
+ if (!res.ok) {
67
+ const text = await res.text();
68
+ const err = { status: res.status, message: text };
69
+ if (res.status >= 400 && res.status < 500) {
70
+ log("error", `${path} failed [${res.status}]`, text);
71
+ throw err;
72
+ }
73
+ if (attempt < MAX_RETRIES - 1) {
74
+ log("warn", `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);
75
+ await sleep(RETRY_DELAYS[attempt]);
76
+ return this.post(path, body, attempt + 1);
77
+ }
78
+ log("error", `${path} failed after ${MAX_RETRIES} attempts`, err);
79
+ throw err;
80
+ }
81
+ return res.json();
82
+ } catch (e) {
83
+ if (e.status === void 0) {
84
+ if (attempt < MAX_RETRIES - 1) {
85
+ log("warn", `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
86
+ await sleep(RETRY_DELAYS[attempt]);
87
+ return this.post(path, body, attempt + 1);
88
+ }
89
+ log("error", `${path} unreachable after ${MAX_RETRIES} attempts`);
90
+ }
91
+ throw e;
63
92
  }
64
- return res.json();
65
93
  }
66
94
  async ingest(product) {
95
+ log("info", "ingesting product", product.name);
67
96
  return this.post("/ingest", { siteId: this.siteId, product });
68
97
  }
69
98
  async ingestBatch(products) {
99
+ log("info", `ingesting batch of ${products.length} products`);
70
100
  return this.post("/ingest/batch", { siteId: this.siteId, products });
71
101
  }
72
102
  async search(query, limit = 10) {
103
+ log("info", "search query", query);
73
104
  return this.post("/search", { query, siteId: this.siteId, limit });
74
105
  }
75
- async pushConfig(config) {
76
- return this.post("/sites/config", __spreadValues({ siteId: this.siteId }, config));
77
- }
78
106
  };
79
107
 
80
- // src/extractor.ts
81
- function getText(el, selector) {
82
- var _a, _b;
83
- if (!selector) return "";
84
- const found = el.querySelector(selector);
85
- return (_b = (_a = found == null ? void 0 : found.textContent) == null ? void 0 : _a.trim()) != null ? _b : "";
86
- }
87
- function getAttr(el, selector, attr) {
88
- var _a, _b;
89
- if (!selector) return "";
90
- const found = el.querySelector(selector);
91
- return (_b = (_a = found == null ? void 0 : found.getAttribute(attr)) == null ? void 0 : _a.trim()) != null ? _b : "";
92
- }
93
- function getAll(el, selector) {
94
- if (!selector) return [];
95
- return Array.from(el.querySelectorAll(selector)).map((n) => {
96
- var _a;
97
- return n.getAttribute("src") || n.getAttribute("href") || ((_a = n.textContent) == null ? void 0 : _a.trim()) || "";
98
- }).filter(Boolean);
99
- }
100
- function parsePrice(raw) {
101
- const num = parseFloat(raw.replace(/[^0-9.]/g, ""));
102
- return isNaN(num) ? void 0 : num;
103
- }
104
- function extractProducts(config) {
105
- const containers = document.querySelectorAll(config.selectorContainer);
106
- if (!containers.length) return [];
107
- const products = [];
108
- containers.forEach((el) => {
109
- var _a;
110
- const name = getText(el, config.selectorName);
111
- const price = getText(el, config.selectorPrice);
112
- const rawUrl = getAttr(el, config.selectorUrl, "href") || el.href || window.location.href;
113
- const url = rawUrl.startsWith("http") ? rawUrl : `${window.location.origin}${rawUrl}`;
114
- if (!name || !price || !url) return;
115
- const product = {
116
- name,
117
- price,
118
- url,
119
- brand: getText(el, config.selectorBrand) || void 0,
120
- description: getText(el, config.selectorDescription) || void 0,
121
- originalPrice: getText(el, config.selectorOriginalPrice) || void 0,
122
- discount: getText(el, config.selectorDiscount) || void 0,
123
- currency: (_a = config.currency) != null ? _a : "KES",
124
- availability: getText(el, config.selectorAvailability) || void 0,
125
- rating: getText(el, config.selectorRating) || void 0,
126
- category: getText(el, config.selectorCategory) || void 0,
127
- images: config.selectorImage ? getAll(el, config.selectorImage) : void 0,
128
- priceNumeric: parsePrice(price),
129
- slug: url.split("/").filter(Boolean).pop()
130
- };
131
- products.push(product);
132
- });
133
- return products;
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;
134
117
  }
135
-
136
- // src/observer.ts
137
- var RouteObserver = class {
138
- constructor() {
139
- this.callbacks = [];
140
- this.current = window.location.href;
141
- this.patchHistory();
142
- window.addEventListener("popstate", () => this.notify());
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
+ }
143
132
  }
144
- patchHistory() {
145
- const notify = () => this.notify();
146
- const wrap = (original) => function(...args) {
147
- original.apply(this, args);
148
- notify();
149
- };
150
- history.pushState = wrap(history.pushState);
151
- history.replaceState = wrap(history.replaceState);
133
+ if (input.priceNumeric !== void 0) {
134
+ priceNumeric = input.priceNumeric;
152
135
  }
153
- notify() {
154
- const next = window.location.href;
155
- if (next !== this.current) {
156
- this.current = next;
157
- this.callbacks.forEach((cb) => cb(next));
158
- }
136
+ let url = input.url || "";
137
+ if (!url && typeof window !== "undefined") {
138
+ url = window.location.href;
159
139
  }
160
- onChange(cb) {
161
- this.callbacks.push(cb);
162
- return () => {
163
- this.callbacks = this.callbacks.filter((fn) => fn !== cb);
164
- };
140
+ let slug = input.slug || input.id || input.productId || "";
141
+ if (!slug && url) {
142
+ slug = url.split("/").filter(Boolean).pop() || "";
165
143
  }
166
- destroy() {
167
- this.callbacks = [];
144
+ if (!slug && name) {
145
+ slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
168
146
  }
169
- };
170
-
171
- // src/client.ts
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
+ }
172
189
  var HuskelClient = class {
173
190
  constructor(config) {
174
- this.config = config;
175
- this.api = new HuskelAPI(config.apiUrl, config.siteId);
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
+ }
176
210
  }
177
- /** Push selectors config to backend once (idempotent) */
178
- async configure() {
179
- await this.api.pushConfig(this.config.selectors);
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;
180
221
  }
181
- /** Scrape current DOM and ingest all products on the page */
182
- async ingestCurrentPage() {
183
- const products = extractProducts(__spreadValues({
184
- siteId: this.config.siteId
185
- }, this.config.selectors));
186
- if (products.length === 0) return [];
187
- await this.api.ingestBatch(products);
188
- return products;
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();
189
231
  }
190
- /** Start watching route changes and auto-ingesting */
191
- start() {
192
- if (!this.config.autoIngest) return;
193
- this.observer = new RouteObserver();
194
- this.unsubscribe = this.observer.onChange(() => {
195
- var _a;
196
- clearTimeout(this.debounceTimer);
197
- this.debounceTimer = setTimeout(() => {
198
- this.ingestCurrentPage().catch(console.error);
199
- }, (_a = this.config.debounceMs) != null ? _a : 600);
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);
200
241
  });
201
- this.ingestCurrentPage().catch(console.error);
242
+ if (this.ingestQueue.length > 0) {
243
+ this.scheduleFlush();
244
+ }
202
245
  }
203
- stop() {
204
- var _a, _b;
205
- (_a = this.unsubscribe) == null ? void 0 : _a.call(this);
206
- (_b = this.observer) == null ? void 0 : _b.destroy();
207
- clearTimeout(this.debounceTimer);
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
+ }
208
272
  }
209
273
  };
210
274
  var instance = null;
@@ -213,19 +277,67 @@ function initHuskel(config) {
213
277
  return instance;
214
278
  }
215
279
  function getHuskelClient() {
216
- 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
+ }
217
290
  return instance;
218
291
  }
219
292
 
220
- // src/hooks/useSearch.ts
293
+ // src/hooks/useHuskel.ts
221
294
  var import_react = require("react");
295
+ function useHuskel(config) {
296
+ const clientRef = (0, import_react.useRef)(null);
297
+ if (!clientRef.current) {
298
+ console.warn("[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.");
299
+ clientRef.current = initHuskel(config);
300
+ }
301
+ return clientRef.current;
302
+ }
303
+
304
+ // src/hooks/useSearch.ts
305
+ var import_react3 = require("react");
306
+
307
+ // src/components/HuskelProvider.tsx
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
222
333
  function useSearch() {
223
- const [results, setResults] = (0, import_react.useState)([]);
224
- const [loading, setLoading] = (0, import_react.useState)(false);
225
- const [error, setError] = (0, import_react.useState)(null);
226
- const abortRef = (0, import_react.useRef)(null);
227
- const search = (0, import_react.useCallback)(async (query, limit = 10) => {
228
- var _a, _b;
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) => {
340
+ var _a, _b, _c;
229
341
  if (!query.trim()) {
230
342
  setResults([]);
231
343
  return;
@@ -235,69 +347,69 @@ function useSearch() {
235
347
  setLoading(true);
236
348
  setError(null);
237
349
  try {
238
- const client = getHuskelClient();
239
350
  const res = await client.api.search(query, limit);
240
351
  setResults((_b = res.results) != null ? _b : []);
241
352
  } catch (e) {
242
- if (e.name !== "AbortError") {
243
- setError(e.message);
244
- }
353
+ setError((_c = e.message) != null ? _c : "Search failed");
245
354
  } finally {
246
355
  setLoading(false);
247
356
  }
248
- }, []);
249
- const clear = (0, import_react.useCallback)(() => {
357
+ }, [client]);
358
+ const clear = (0, import_react3.useCallback)(() => {
250
359
  setResults([]);
251
360
  setError(null);
252
361
  }, []);
253
362
  return { results, loading, error, search, clear };
254
363
  }
255
364
 
256
- // src/hooks/useHuskel.ts
257
- var import_react2 = require("react");
258
- function useHuskel(options) {
259
- const clientRef = (0, import_react2.useRef)(null);
260
- (0, import_react2.useEffect)(() => {
365
+ // src/hooks/useIngest.ts
366
+ var import_react4 = require("react");
367
+ function useIngest() {
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) => {
261
372
  var _a;
262
- const client = initHuskel(options);
263
- clientRef.current = client;
264
- client.configure().then(() => {
265
- client.start();
266
- }).catch((_a = options.onError) != null ? _a : console.error);
267
- return () => client.stop();
268
- }, []);
269
- return clientRef.current;
373
+ setLoading(true);
374
+ setError(null);
375
+ try {
376
+ await client.queueIngest(product);
377
+ } catch (e) {
378
+ setError((_a = e.message) != null ? _a : "Ingest failed");
379
+ } finally {
380
+ setLoading(false);
381
+ }
382
+ }, [client]);
383
+ const ingestBatch = (0, import_react4.useCallback)(async (products) => {
384
+ var _a;
385
+ if (!products.length) return;
386
+ setLoading(true);
387
+ setError(null);
388
+ try {
389
+ await client.queueIngestBatch(products);
390
+ } catch (e) {
391
+ setError((_a = e.message) != null ? _a : "Batch ingest failed");
392
+ } finally {
393
+ setLoading(false);
394
+ }
395
+ }, [client]);
396
+ return { ingest, ingestBatch, loading, error };
270
397
  }
271
398
 
272
399
  // src/components/SearchBar.tsx
273
- var import_react3 = require("react");
274
- var import_jsx_runtime = require("react/jsx-runtime");
275
- var DEFAULT_STYLES = `
276
- .huskel-search-wrap { position: relative; width: 100%; font-family: inherit; }
277
- .huskel-search-input {
278
- width: 100%; padding: 10px 16px; font-size: 15px;
279
- border: 1.5px solid #e2e2e2; border-radius: 8px;
280
- outline: none; box-sizing: border-box; background: #fff;
281
- transition: border-color 0.2s;
282
- }
283
- .huskel-search-input:focus { border-color: #f47c3c; }
284
- .huskel-search-dropdown {
285
- position: absolute; top: calc(100% + 6px); left: 0; right: 0;
286
- background: #fff; border: 1px solid #e2e2e2; border-radius: 8px;
287
- box-shadow: 0 8px 24px rgba(0,0,0,0.10); z-index: 9999;
288
- max-height: 360px; overflow-y: auto;
289
- }
290
- .huskel-search-item {
291
- display: flex; align-items: center; gap: 12px;
292
- padding: 10px 14px; cursor: pointer; transition: background 0.15s;
293
- }
294
- .huskel-search-item:hover { background: #faf5f1; }
295
- .huskel-search-item img { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
296
- .huskel-search-item-info { flex: 1; min-width: 0; }
297
- .huskel-search-item-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
298
- .huskel-search-item-price { font-size: 13px; color: #f47c3c; margin-top: 2px; }
299
- .huskel-search-empty { padding: 16px; text-align: center; color: #888; font-size: 14px; }
300
- .huskel-search-loading { padding: 16px; text-align: center; color: #aaa; font-size: 13px; }
400
+ var import_react5 = require("react");
401
+ var import_jsx_runtime2 = require("react/jsx-runtime");
402
+ var S = `
403
+ .hsk-wrap{position:relative;width:100%;font-family:inherit}
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}
405
+ .hsk-input:focus{border-color:#f47c3c}
406
+ .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}
407
+ .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
408
+ .hsk-item:hover{background:#faf5f1}
409
+ .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
410
+ .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
411
+ .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
412
+ .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
301
413
  `;
302
414
  function SearchBar({
303
415
  placeholder = "Search for what you want \u2014 how you want",
@@ -305,31 +417,31 @@ function SearchBar({
305
417
  debounceMs = 300,
306
418
  onSelect,
307
419
  className,
420
+ inputClassName,
421
+ dropdownClassName,
308
422
  renderResult
309
423
  }) {
310
- const [query, setQuery] = (0, import_react3.useState)("");
311
- const [open, setOpen] = (0, import_react3.useState)(false);
424
+ const [query, setQuery] = (0, import_react5.useState)("");
425
+ const [open, setOpen] = (0, import_react5.useState)(false);
312
426
  const { results, loading, search, clear } = useSearch();
313
- const timerRef = (0, import_react3.useRef)();
314
- const wrapRef = (0, import_react3.useRef)(null);
315
- (0, import_react3.useEffect)(() => {
316
- clearTimeout(timerRef.current);
427
+ const timer = (0, import_react5.useRef)();
428
+ const wrap = (0, import_react5.useRef)(null);
429
+ (0, import_react5.useEffect)(() => {
430
+ clearTimeout(timer.current);
317
431
  if (!query.trim()) {
318
432
  clear();
319
433
  setOpen(false);
320
434
  return;
321
435
  }
322
- timerRef.current = setTimeout(() => {
436
+ timer.current = setTimeout(() => {
323
437
  search(query, limit);
324
438
  setOpen(true);
325
439
  }, debounceMs);
326
- return () => clearTimeout(timerRef.current);
327
- }, [query]);
328
- (0, import_react3.useEffect)(() => {
440
+ return () => clearTimeout(timer.current);
441
+ }, [query, search, clear, limit, debounceMs]);
442
+ (0, import_react5.useEffect)(() => {
329
443
  const handler = (e) => {
330
- if (wrapRef.current && !wrapRef.current.contains(e.target)) {
331
- setOpen(false);
332
- }
444
+ if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
333
445
  };
334
446
  document.addEventListener("mousedown", handler);
335
447
  return () => document.removeEventListener("mousedown", handler);
@@ -339,13 +451,13 @@ function SearchBar({
339
451
  setQuery(r.product.name);
340
452
  onSelect == null ? void 0 : onSelect(r);
341
453
  };
342
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
343
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: DEFAULT_STYLES }),
344
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `huskel-search-wrap ${className != null ? className : ""}`, ref: wrapRef, children: [
345
- /* @__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)(
346
458
  "input",
347
459
  {
348
- className: "huskel-search-input",
460
+ className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
349
461
  type: "text",
350
462
  value: query,
351
463
  placeholder,
@@ -353,9 +465,9 @@ function SearchBar({
353
465
  onFocus: () => results.length && setOpen(true)
354
466
  }
355
467
  ),
356
- open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "huskel-search-dropdown", children: [
357
- loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "huskel-search-loading", children: "Searching\u2026" }),
358
- !loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "huskel-search-empty", 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: [
359
471
  'No results for "',
360
472
  query,
361
473
  '"'
@@ -363,11 +475,11 @@ function SearchBar({
363
475
  results.map(
364
476
  (r) => {
365
477
  var _a, _b;
366
- 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: "huskel-search-item", onClick: () => handleSelect(r), children: [
367
- ((_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 }),
368
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "huskel-search-item-info", children: [
369
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "huskel-search-item-name", children: r.product.name }),
370
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "huskel-search-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: [
371
483
  (_b = r.product.currency) != null ? _b : "KES",
372
484
  " ",
373
485
  r.product.price
@@ -382,27 +494,20 @@ function SearchBar({
382
494
  }
383
495
 
384
496
  // src/components/Sparkle.tsx
385
- var import_react4 = require("react");
386
- var import_jsx_runtime2 = require("react/jsx-runtime");
387
- var STYLES = `
388
- .huskel-sparkle-btn {
389
- display: inline-flex; align-items: center; gap: 5px;
390
- padding: 4px 10px; font-size: 12px; font-weight: 600;
391
- background: #f47c3c; color: #fff; border: none;
392
- border-radius: 20px; cursor: pointer; letter-spacing: 0.02em;
393
- transition: opacity 0.2s, transform 0.15s;
394
- }
395
- .huskel-sparkle-btn:hover { opacity: 0.88; transform: scale(1.04); }
396
- .huskel-sparkle-btn:disabled { opacity: 0.5; cursor: not-allowed; }
397
- .huskel-sparkle-icon { font-size: 13px; }
497
+ var import_react6 = require("react");
498
+ var import_jsx_runtime3 = require("react/jsx-runtime");
499
+ var S2 = `
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}
501
+ .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
502
+ .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
398
503
  `;
399
- function Sparkle({ productName, className, onResult }) {
400
- const [loading, setLoading] = (0, import_react4.useState)(false);
504
+ function Sparkle({ productName, limit = 5, onResult, className }) {
505
+ const client = useHuskelContext();
506
+ const [loading, setLoading] = (0, import_react6.useState)(false);
401
507
  const handleClick = async () => {
402
508
  setLoading(true);
403
509
  try {
404
- const client = getHuskelClient();
405
- const res = await client.api.search(productName, 5);
510
+ const res = await client.api.search(productName, limit);
406
511
  onResult == null ? void 0 : onResult(res.results);
407
512
  } catch (e) {
408
513
  console.error("[Huskel Sparkle]", e);
@@ -410,31 +515,24 @@ function Sparkle({ productName, className, onResult }) {
410
515
  setLoading(false);
411
516
  }
412
517
  };
413
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
414
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: STYLES }),
415
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
416
- "button",
417
- {
418
- className: `huskel-sparkle-btn ${className != null ? className : ""}`,
419
- onClick: handleClick,
420
- disabled: loading,
421
- title: "Find similar with AI",
422
- children: [
423
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "huskel-sparkle-icon", children: "\u2726" }),
424
- loading ? "Finding\u2026" : "Similar"
425
- ]
426
- }
427
- )
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: [
521
+ "\u2726 ",
522
+ loading ? "Finding\u2026" : "Similar"
523
+ ] })
428
524
  ] });
429
525
  }
430
526
  // Annotate the CommonJS export names for ESM import in node:
431
527
  0 && (module.exports = {
432
528
  HuskelAPI,
433
529
  HuskelClient,
530
+ HuskelProvider,
434
531
  SearchBar,
435
532
  Sparkle,
436
533
  getHuskelClient,
437
534
  initHuskel,
438
535
  useHuskel,
536
+ useIngest,
439
537
  useSearch
440
538
  });