@huskel/sdk 0.1.0 → 0.2.0

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 });
@@ -41,170 +27,87 @@ __export(index_exports, {
41
27
  getHuskelClient: () => getHuskelClient,
42
28
  initHuskel: () => initHuskel,
43
29
  useHuskel: () => useHuskel,
30
+ useIngest: () => useIngest,
44
31
  useSearch: () => useSearch
45
32
  });
46
33
  module.exports = __toCommonJS(index_exports);
47
34
 
48
35
  // src/api.ts
36
+ var MAX_RETRIES = 3;
37
+ var RETRY_DELAYS = [500, 1e3, 2e3];
38
+ function log(level, msg, data) {
39
+ const prefix = "[Huskel]";
40
+ if (level === "error") console.error(prefix, msg, data != null ? data : "");
41
+ else if (level === "warn") console.warn(prefix, msg, data != null ? data : "");
42
+ else console.log(prefix, msg, data != null ? data : "");
43
+ }
44
+ async function sleep(ms) {
45
+ return new Promise((r) => setTimeout(r, ms));
46
+ }
49
47
  var HuskelAPI = class {
50
- constructor(apiUrl, siteId) {
48
+ constructor(apiUrl, siteId, apiToken) {
51
49
  this.apiUrl = apiUrl;
52
50
  this.siteId = siteId;
51
+ this.apiToken = apiToken;
53
52
  }
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}`);
53
+ async post(path, body, attempt = 0) {
54
+ const url = `${this.apiUrl}${path}`;
55
+ try {
56
+ const res = await fetch(url, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ "X-Huskel-Token": this.apiToken,
61
+ "X-Huskel-Site": this.siteId
62
+ },
63
+ body: JSON.stringify(body)
64
+ });
65
+ if (!res.ok) {
66
+ const text = await res.text();
67
+ const err = { status: res.status, message: text };
68
+ if (res.status >= 400 && res.status < 500) {
69
+ log("error", `${path} failed [${res.status}]`, text);
70
+ throw err;
71
+ }
72
+ if (attempt < MAX_RETRIES - 1) {
73
+ log("warn", `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);
74
+ await sleep(RETRY_DELAYS[attempt]);
75
+ return this.post(path, body, attempt + 1);
76
+ }
77
+ log("error", `${path} failed after ${MAX_RETRIES} attempts`, err);
78
+ throw err;
79
+ }
80
+ return res.json();
81
+ } catch (e) {
82
+ if (e.status === void 0) {
83
+ if (attempt < MAX_RETRIES - 1) {
84
+ log("warn", `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
85
+ await sleep(RETRY_DELAYS[attempt]);
86
+ return this.post(path, body, attempt + 1);
87
+ }
88
+ log("error", `${path} unreachable after ${MAX_RETRIES} attempts`);
89
+ }
90
+ throw e;
63
91
  }
64
- return res.json();
65
92
  }
66
93
  async ingest(product) {
94
+ log("info", "ingesting product", product.name);
67
95
  return this.post("/ingest", { siteId: this.siteId, product });
68
96
  }
69
97
  async ingestBatch(products) {
98
+ log("info", `ingesting batch of ${products.length} products`);
70
99
  return this.post("/ingest/batch", { siteId: this.siteId, products });
71
100
  }
72
101
  async search(query, limit = 10) {
102
+ log("info", "search query", query);
73
103
  return this.post("/search", { query, siteId: this.siteId, limit });
74
104
  }
75
- async pushConfig(config) {
76
- return this.post("/sites/config", __spreadValues({ siteId: this.siteId }, config));
77
- }
78
- };
79
-
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;
134
- }
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());
143
- }
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);
152
- }
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
- }
159
- }
160
- onChange(cb) {
161
- this.callbacks.push(cb);
162
- return () => {
163
- this.callbacks = this.callbacks.filter((fn) => fn !== cb);
164
- };
165
- }
166
- destroy() {
167
- this.callbacks = [];
168
- }
169
105
  };
170
106
 
171
107
  // src/client.ts
172
108
  var HuskelClient = class {
173
109
  constructor(config) {
174
- this.config = config;
175
- this.api = new HuskelAPI(config.apiUrl, config.siteId);
176
- }
177
- /** Push selectors config to backend once (idempotent) */
178
- async configure() {
179
- await this.api.pushConfig(this.config.selectors);
180
- }
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;
189
- }
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);
200
- });
201
- this.ingestCurrentPage().catch(console.error);
202
- }
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);
110
+ this.api = new HuskelAPI(config.apiUrl, config.siteId, config.apiToken);
208
111
  }
209
112
  };
210
113
  var instance = null;
@@ -217,15 +120,25 @@ function getHuskelClient() {
217
120
  return instance;
218
121
  }
219
122
 
220
- // src/hooks/useSearch.ts
123
+ // src/hooks/useHuskel.ts
221
124
  var import_react = require("react");
125
+ function useHuskel(config) {
126
+ const clientRef = (0, import_react.useRef)(null);
127
+ if (!clientRef.current) {
128
+ clientRef.current = initHuskel(config);
129
+ }
130
+ return clientRef.current;
131
+ }
132
+
133
+ // src/hooks/useSearch.ts
134
+ var import_react2 = require("react");
222
135
  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;
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) => {
141
+ var _a, _b, _c;
229
142
  if (!query.trim()) {
230
143
  setResults([]);
231
144
  return;
@@ -235,69 +148,68 @@ function useSearch() {
235
148
  setLoading(true);
236
149
  setError(null);
237
150
  try {
238
- const client = getHuskelClient();
239
- const res = await client.api.search(query, limit);
151
+ const res = await getHuskelClient().api.search(query, limit);
240
152
  setResults((_b = res.results) != null ? _b : []);
241
153
  } catch (e) {
242
- if (e.name !== "AbortError") {
243
- setError(e.message);
244
- }
154
+ setError((_c = e.message) != null ? _c : "Search failed");
245
155
  } finally {
246
156
  setLoading(false);
247
157
  }
248
158
  }, []);
249
- const clear = (0, import_react.useCallback)(() => {
159
+ const clear = (0, import_react2.useCallback)(() => {
250
160
  setResults([]);
251
161
  setError(null);
252
162
  }, []);
253
163
  return { results, loading, error, search, clear };
254
164
  }
255
165
 
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)(() => {
166
+ // src/hooks/useIngest.ts
167
+ var import_react3 = require("react");
168
+ 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) => {
261
172
  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();
173
+ setLoading(true);
174
+ setError(null);
175
+ try {
176
+ await getHuskelClient().api.ingest(product);
177
+ } catch (e) {
178
+ setError((_a = e.message) != null ? _a : "Ingest failed");
179
+ } finally {
180
+ setLoading(false);
181
+ }
268
182
  }, []);
269
- return clientRef.current;
183
+ const ingestBatch = (0, import_react3.useCallback)(async (products) => {
184
+ var _a;
185
+ if (!products.length) return;
186
+ setLoading(true);
187
+ setError(null);
188
+ try {
189
+ await getHuskelClient().api.ingestBatch(products);
190
+ } catch (e) {
191
+ setError((_a = e.message) != null ? _a : "Batch ingest failed");
192
+ } finally {
193
+ setLoading(false);
194
+ }
195
+ }, []);
196
+ return { ingest, ingestBatch, loading, error };
270
197
  }
271
198
 
272
199
  // src/components/SearchBar.tsx
273
- var import_react3 = require("react");
200
+ var import_react4 = require("react");
274
201
  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; }
202
+ var S = `
203
+ .hsk-wrap{position:relative;width:100%;font-family:inherit}
204
+ .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}
205
+ .hsk-input:focus{border-color:#f47c3c}
206
+ .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}
207
+ .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
208
+ .hsk-item:hover{background:#faf5f1}
209
+ .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
210
+ .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
211
+ .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
212
+ .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
301
213
  `;
302
214
  function SearchBar({
303
215
  placeholder = "Search for what you want \u2014 how you want",
@@ -305,31 +217,31 @@ function SearchBar({
305
217
  debounceMs = 300,
306
218
  onSelect,
307
219
  className,
220
+ inputClassName,
221
+ dropdownClassName,
308
222
  renderResult
309
223
  }) {
310
- const [query, setQuery] = (0, import_react3.useState)("");
311
- const [open, setOpen] = (0, import_react3.useState)(false);
224
+ const [query, setQuery] = (0, import_react4.useState)("");
225
+ const [open, setOpen] = (0, import_react4.useState)(false);
312
226
  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);
227
+ const timer = (0, import_react4.useRef)();
228
+ const wrap = (0, import_react4.useRef)(null);
229
+ (0, import_react4.useEffect)(() => {
230
+ clearTimeout(timer.current);
317
231
  if (!query.trim()) {
318
232
  clear();
319
233
  setOpen(false);
320
234
  return;
321
235
  }
322
- timerRef.current = setTimeout(() => {
236
+ timer.current = setTimeout(() => {
323
237
  search(query, limit);
324
238
  setOpen(true);
325
239
  }, debounceMs);
326
- return () => clearTimeout(timerRef.current);
240
+ return () => clearTimeout(timer.current);
327
241
  }, [query]);
328
- (0, import_react3.useEffect)(() => {
242
+ (0, import_react4.useEffect)(() => {
329
243
  const handler = (e) => {
330
- if (wrapRef.current && !wrapRef.current.contains(e.target)) {
331
- setOpen(false);
332
- }
244
+ if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
333
245
  };
334
246
  document.addEventListener("mousedown", handler);
335
247
  return () => document.removeEventListener("mousedown", handler);
@@ -340,12 +252,12 @@ function SearchBar({
340
252
  onSelect == null ? void 0 : onSelect(r);
341
253
  };
342
254
  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: [
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: [
345
257
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
258
  "input",
347
259
  {
348
- className: "huskel-search-input",
260
+ className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
349
261
  type: "text",
350
262
  value: query,
351
263
  placeholder,
@@ -353,9 +265,9 @@ function SearchBar({
353
265
  onFocus: () => results.length && setOpen(true)
354
266
  }
355
267
  ),
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: [
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: [
359
271
  'No results for "',
360
272
  query,
361
273
  '"'
@@ -363,11 +275,11 @@ function SearchBar({
363
275
  results.map(
364
276
  (r) => {
365
277
  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: [
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: [
367
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 }),
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: [
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: [
371
283
  (_b = r.product.currency) != null ? _b : "KES",
372
284
  " ",
373
285
  r.product.price
@@ -382,27 +294,19 @@ function SearchBar({
382
294
  }
383
295
 
384
296
  // src/components/Sparkle.tsx
385
- var import_react4 = require("react");
297
+ var import_react5 = require("react");
386
298
  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; }
299
+ var S2 = `
300
+ .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
+ .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
302
+ .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
398
303
  `;
399
- function Sparkle({ productName, className, onResult }) {
400
- const [loading, setLoading] = (0, import_react4.useState)(false);
304
+ function Sparkle({ productName, limit = 5, onResult, className }) {
305
+ const [loading, setLoading] = (0, import_react5.useState)(false);
401
306
  const handleClick = async () => {
402
307
  setLoading(true);
403
308
  try {
404
- const client = getHuskelClient();
405
- const res = await client.api.search(productName, 5);
309
+ const res = await getHuskelClient().api.search(productName, limit);
406
310
  onResult == null ? void 0 : onResult(res.results);
407
311
  } catch (e) {
408
312
  console.error("[Huskel Sparkle]", e);
@@ -411,20 +315,11 @@ function Sparkle({ productName, className, onResult }) {
411
315
  }
412
316
  };
413
317
  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
- )
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: [
320
+ "\u2726 ",
321
+ loading ? "Finding\u2026" : "Similar"
322
+ ] })
428
323
  ] });
429
324
  }
430
325
  // Annotate the CommonJS export names for ESM import in node:
@@ -436,5 +331,6 @@ function Sparkle({ productName, className, onResult }) {
436
331
  getHuskelClient,
437
332
  initHuskel,
438
333
  useHuskel,
334
+ useIngest,
439
335
  useSearch
440
336
  });