@pro6pp/infer-react 0.0.2-beta.1 → 0.0.2-beta.11

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
@@ -1,50 +1,785 @@
1
- "use strict";
2
1
  var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/index.tsx
6
+ import React, {
7
+ useState,
8
+ useMemo,
9
+ useEffect,
10
+ useRef,
11
+ forwardRef,
12
+ useImperativeHandle
13
+ } from "react";
14
+
15
+ // ../core/src/core.ts
16
+ var DEFAULTS = {
17
+ API_URL: "https://api.pro6pp.nl/v2",
18
+ LIMIT: 20,
19
+ DEBOUNCE_MS: 150,
20
+ MIN_DEBOUNCE_MS: 50,
21
+ MAX_RETRIES: 0
22
+ };
23
+ var PATTERNS = {
24
+ DIGITS_1_3: /^[0-9]{1,3}$/
9
25
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ var INITIAL_STATE = {
27
+ query: "",
28
+ stage: null,
29
+ cities: [],
30
+ streets: [],
31
+ suggestions: [],
32
+ isValid: false,
33
+ isError: false,
34
+ isLoading: false,
35
+ hasMore: false,
36
+ selectedSuggestionIndex: -1
37
+ };
38
+ var InferCore = class {
39
+ /**
40
+ * Initializes a new instance of the Infer engine.
41
+ * @param config The configuration object including API keys and callbacks.
42
+ */
43
+ constructor(config) {
44
+ __publicField(this, "country");
45
+ __publicField(this, "authKey");
46
+ __publicField(this, "explicitApiUrl");
47
+ __publicField(this, "baseLimit");
48
+ __publicField(this, "currentLimit");
49
+ __publicField(this, "maxRetries");
50
+ __publicField(this, "fetcher");
51
+ __publicField(this, "onStateChange");
52
+ __publicField(this, "onSelect");
53
+ /**
54
+ * The current read-only state of the engine.
55
+ * Use `onStateChange` to react to updates.
56
+ */
57
+ __publicField(this, "state");
58
+ __publicField(this, "abortController", null);
59
+ __publicField(this, "debouncedFetch");
60
+ this.country = config.country;
61
+ this.authKey = config.authKey;
62
+ this.explicitApiUrl = config.apiUrl;
63
+ this.baseLimit = config.limit || DEFAULTS.LIMIT;
64
+ this.currentLimit = this.baseLimit;
65
+ const configRetries = config.maxRetries !== void 0 ? config.maxRetries : DEFAULTS.MAX_RETRIES;
66
+ this.maxRetries = Math.max(0, Math.min(configRetries, 10));
67
+ this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
68
+ this.onStateChange = config.onStateChange || (() => {
69
+ });
70
+ this.onSelect = config.onSelect || (() => {
71
+ });
72
+ this.state = { ...INITIAL_STATE };
73
+ const configDebounce = config.debounceMs !== void 0 ? config.debounceMs : DEFAULTS.DEBOUNCE_MS;
74
+ const debounceTime = Math.max(configDebounce, DEFAULTS.MIN_DEBOUNCE_MS);
75
+ this.debouncedFetch = this.debounce((val) => this.executeFetch(val), debounceTime);
76
+ }
77
+ /**
78
+ * Processes new text input from the user.
79
+ * Triggers a debounced API request and updates the internal state.
80
+ * @param value The raw string from the input field.
81
+ */
82
+ handleInput(value) {
83
+ this.currentLimit = this.baseLimit;
84
+ const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
85
+ this.updateState({
86
+ query: value,
87
+ isValid: false,
88
+ isLoading: !!value.trim(),
89
+ selectedSuggestionIndex: -1,
90
+ hasMore: false
91
+ });
92
+ if (isEditingFinal) {
93
+ this.onSelect(null);
94
+ }
95
+ this.debouncedFetch(value);
96
+ }
97
+ /**
98
+ * Increases the current limit and re-fetches the query to show more results.
99
+ */
100
+ loadMore() {
101
+ if (this.state.isLoading) return;
102
+ this.currentLimit += this.baseLimit;
103
+ this.updateState({ isLoading: true });
104
+ this.executeFetch(this.state.query);
105
+ }
106
+ /**
107
+ * Handles keyboard events for the input field.
108
+ * Supports:
109
+ * - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
110
+ * - `Enter`: Select the currently highlighted suggestion.
111
+ * - `Space`: Automatically inserts a comma if a numeric street number is detected.
112
+ * @param event The keyboard event from the input element.
113
+ */
114
+ handleKeyDown(event) {
115
+ const target = event.target;
116
+ if (!target) return;
117
+ const totalItems = this.state.cities.length + this.state.streets.length + this.state.suggestions.length;
118
+ if (totalItems > 0) {
119
+ if (event.key === "ArrowDown") {
120
+ event.preventDefault();
121
+ let nextIndex = this.state.selectedSuggestionIndex + 1;
122
+ if (nextIndex >= totalItems) {
123
+ nextIndex = 0;
124
+ }
125
+ this.updateState({ selectedSuggestionIndex: nextIndex });
126
+ return;
127
+ }
128
+ if (event.key === "ArrowUp") {
129
+ event.preventDefault();
130
+ let nextIndex = this.state.selectedSuggestionIndex - 1;
131
+ if (nextIndex < 0) {
132
+ nextIndex = totalItems - 1;
133
+ }
134
+ this.updateState({ selectedSuggestionIndex: nextIndex });
135
+ return;
136
+ }
137
+ if (event.key === "Enter" && this.state.selectedSuggestionIndex >= 0) {
138
+ event.preventDefault();
139
+ const allItems = [...this.state.cities, ...this.state.streets, ...this.state.suggestions];
140
+ const item = allItems[this.state.selectedSuggestionIndex];
141
+ if (item) {
142
+ this.selectItem(item);
143
+ this.updateState({ selectedSuggestionIndex: -1 });
144
+ }
145
+ return;
146
+ }
147
+ }
148
+ const val = target.value;
149
+ if (event.key === " " && this.shouldAutoInsertComma(val)) {
150
+ event.preventDefault();
151
+ const next = `${val.trim()}, `;
152
+ this.updateQueryAndFetch(next);
153
+ }
154
+ }
155
+ /**
156
+ * Manually selects a suggestion or a string value.
157
+ * This is typically called when a user clicks a suggestion in the UI.
158
+ * @param item The suggestion object or string to select.
159
+ * @returns boolean True if the selection is a final address.
160
+ */
161
+ selectItem(item) {
162
+ this.debouncedFetch.cancel();
163
+ if (this.abortController) {
164
+ this.abortController.abort();
165
+ }
166
+ const label = typeof item === "string" ? item : item.label;
167
+ let logicValue = label;
168
+ if (typeof item !== "string" && typeof item.value === "string") {
169
+ logicValue = item.value;
170
+ }
171
+ const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
172
+ const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
173
+ if (this.state.stage === "final" || isFullResult) {
174
+ let finalQuery = label;
175
+ if (valueObj && Object.keys(valueObj).length > 0) {
176
+ const { street, street_number, city } = valueObj;
177
+ if (street && street_number && city) {
178
+ finalQuery = `${street} ${street_number}, ${city}`;
179
+ }
180
+ }
181
+ this.finishSelection(finalQuery, valueObj);
182
+ return true;
183
+ }
184
+ const subtitle = typeof item !== "string" ? item.subtitle : null;
185
+ this.processSelection(logicValue, subtitle);
186
+ return false;
187
+ }
188
+ shouldAutoInsertComma(currentVal) {
189
+ const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
190
+ if (isStartOfSegmentAndNumeric) return true;
191
+ if (this.state.stage === "street_number") {
192
+ const currentFragment = this.getCurrentFragment(currentVal);
193
+ return PATTERNS.DIGITS_1_3.test(currentFragment);
194
+ }
195
+ return false;
196
+ }
197
+ finishSelection(label, value) {
198
+ this.updateState({
199
+ query: label,
200
+ suggestions: [],
201
+ cities: [],
202
+ streets: [],
203
+ isValid: true,
204
+ stage: "final",
205
+ hasMore: false
206
+ });
207
+ this.onSelect(value || label);
208
+ }
209
+ processSelection(text, subtitle) {
210
+ const { stage, query } = this.state;
211
+ let nextQuery = query;
212
+ const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
213
+ if (isContextualSelection) {
214
+ if (stage === "city") {
215
+ nextQuery = `${subtitle}, ${text}, `;
216
+ } else {
217
+ const prefix = this.getQueryPrefix(query);
218
+ nextQuery = prefix ? `${prefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
219
+ }
220
+ this.updateQueryAndFetch(nextQuery);
221
+ return;
222
+ }
223
+ if (stage === "direct" || stage === "addition") {
224
+ this.finishSelection(text);
225
+ return;
226
+ }
227
+ const hasComma = query.includes(",");
228
+ const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "street_number_first");
229
+ if (isFirstSegment) {
230
+ nextQuery = `${text}, `;
231
+ } else {
232
+ nextQuery = this.replaceLastSegment(query, text);
233
+ if (stage !== "street_number") {
234
+ nextQuery += ", ";
235
+ }
236
+ }
237
+ this.updateQueryAndFetch(nextQuery);
238
+ }
239
+ executeFetch(val, attempt = 0) {
240
+ const text = (val || "").toString();
241
+ if (!text.trim()) {
242
+ this.abortController?.abort();
243
+ this.resetState();
244
+ return;
245
+ }
246
+ if (attempt === 0) {
247
+ this.updateState({ isError: false });
248
+ if (this.abortController) this.abortController.abort();
249
+ this.abortController = new AbortController();
250
+ }
251
+ const currentSignal = this.abortController?.signal;
252
+ const baseUrl = this.explicitApiUrl ? this.explicitApiUrl : `${DEFAULTS.API_URL}/infer/${this.country.toLowerCase()}`;
253
+ const params = new URLSearchParams({
254
+ country: this.country.toLowerCase(),
255
+ query: text,
256
+ limit: this.currentLimit.toString()
257
+ });
258
+ if (this.authKey) {
259
+ params.set("authKey", this.authKey);
260
+ }
261
+ const separator = baseUrl.includes("?") ? "&" : "?";
262
+ const finalUrl = `${baseUrl}${separator}${params.toString()}`;
263
+ this.fetcher(finalUrl, { signal: currentSignal }).then((res) => {
264
+ if (!res.ok) {
265
+ if (attempt < this.maxRetries && (res.status >= 500 || res.status === 429)) {
266
+ return this.retry(val, attempt, currentSignal);
267
+ }
268
+ throw new Error("Network error");
269
+ }
270
+ return res.json();
271
+ }).then((data) => {
272
+ if (data) this.mapResponseToState(data);
273
+ }).catch((e) => {
274
+ if (e.name === "AbortError") return;
275
+ if (attempt < this.maxRetries) {
276
+ return this.retry(val, attempt, currentSignal);
277
+ }
278
+ this.updateState({ isError: true, isLoading: false });
279
+ });
280
+ }
281
+ retry(val, attempt, signal) {
282
+ if (signal?.aborted) return;
283
+ const delay = Math.pow(2, attempt) * 200;
284
+ setTimeout(() => {
285
+ if (!signal?.aborted) {
286
+ this.executeFetch(val, attempt + 1);
287
+ }
288
+ }, delay);
289
+ }
290
+ mapResponseToState(data) {
291
+ const newState = {
292
+ stage: data.stage,
293
+ isLoading: false
294
+ };
295
+ const rawSuggestions = data.suggestions || [];
296
+ const uniqueSuggestions = [];
297
+ const seen = /* @__PURE__ */ new Set();
298
+ for (const item of rawSuggestions) {
299
+ const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
300
+ if (!seen.has(key)) {
301
+ seen.add(key);
302
+ uniqueSuggestions.push(item);
303
+ }
304
+ }
305
+ const totalCount = uniqueSuggestions.length + (data.cities?.length || 0) + (data.streets?.length || 0);
306
+ newState.hasMore = totalCount >= this.currentLimit;
307
+ if (data.stage === "mixed") {
308
+ newState.cities = data.cities || [];
309
+ newState.streets = data.streets || [];
310
+ newState.suggestions = [];
311
+ } else {
312
+ newState.suggestions = uniqueSuggestions;
313
+ newState.cities = [];
314
+ newState.streets = [];
315
+ }
316
+ newState.isValid = data.stage === "final";
317
+ this.updateState(newState);
318
+ }
319
+ updateQueryAndFetch(nextQuery) {
320
+ this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
321
+ this.updateState({ isLoading: true, isValid: false, hasMore: false });
322
+ this.debouncedFetch(nextQuery);
323
+ }
324
+ replaceLastSegment(fullText, newSegment) {
325
+ const lastCommaIndex = fullText.lastIndexOf(",");
326
+ if (lastCommaIndex === -1) return newSegment;
327
+ return `${fullText.slice(0, lastCommaIndex + 1)} ${newSegment}`.trim();
328
+ }
329
+ getQueryPrefix(q) {
330
+ const lastComma = q.lastIndexOf(",");
331
+ return lastComma === -1 ? "" : q.slice(0, lastComma + 1).trimEnd();
332
+ }
333
+ getCurrentFragment(q) {
334
+ return (q.split(",").slice(-1)[0] ?? "").trim();
335
+ }
336
+ resetState() {
337
+ this.updateState({ ...INITIAL_STATE, query: this.state.query });
338
+ }
339
+ updateState(updates) {
340
+ this.state = { ...this.state, ...updates };
341
+ this.onStateChange(this.state);
342
+ }
343
+ debounce(func, wait) {
344
+ let timeout;
345
+ const debounced = (...args) => {
346
+ if (timeout) clearTimeout(timeout);
347
+ timeout = setTimeout(() => func.apply(this, args), wait);
348
+ };
349
+ debounced.cancel = () => {
350
+ if (timeout) {
351
+ clearTimeout(timeout);
352
+ timeout = void 0;
353
+ }
354
+ };
355
+ return debounced;
356
+ }
357
+ };
358
+
359
+ // ../core/src/highlight.ts
360
+ function getHighlightSegments(text, query) {
361
+ if (!query || !text) return [{ text, match: false }];
362
+ const segments = [];
363
+ const normalizedText = text.toLowerCase();
364
+ const normalizedQuery = query.toLowerCase();
365
+ let queryCursor = 0;
366
+ let unmatchedCursor = 0;
367
+ for (let textCursor = 0; textCursor < text.length; textCursor++) {
368
+ const isMatch = queryCursor < query.length && normalizedText[textCursor] === normalizedQuery[queryCursor];
369
+ if (!isMatch) continue;
370
+ const hasPrecedingUnmatched = textCursor > unmatchedCursor;
371
+ if (hasPrecedingUnmatched) {
372
+ segments.push({ text: text.slice(unmatchedCursor, textCursor), match: false });
373
+ }
374
+ segments.push({ text: text[textCursor], match: true });
375
+ queryCursor++;
376
+ unmatchedCursor = textCursor + 1;
377
+ }
378
+ const hasRemainingText = unmatchedCursor < text.length;
379
+ if (hasRemainingText) {
380
+ segments.push({ text: text.slice(unmatchedCursor), match: false });
381
+ }
382
+ const isFullMatch = queryCursor === query.length;
383
+ if (!isFullMatch) {
384
+ return [{ text, match: false }];
385
+ }
386
+ return segments;
387
+ }
388
+
389
+ // ../core/src/default-styles.ts
390
+ var DEFAULT_STYLES = `
391
+ .pro6pp-wrapper {
392
+ position: relative;
393
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
394
+ box-sizing: border-box;
395
+ width: 100%;
396
+ -webkit-tap-highlight-color: transparent;
397
+ }
398
+ .pro6pp-wrapper * {
399
+ box-sizing: border-box;
400
+ }
401
+ .pro6pp-input {
402
+ width: 100%;
403
+ padding: 12px 14px;
404
+ padding-right: 48px;
405
+ border: 1px solid #e0e0e0;
406
+ border-radius: 8px;
407
+ font-size: 16px;
408
+ line-height: 1.5;
409
+ appearance: none;
410
+ transition: border-color 0.2s, box-shadow 0.2s;
411
+ }
412
+ .pro6pp-input:focus {
413
+ outline: none;
414
+ border-color: #3b82f6;
415
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
416
+ }
417
+
418
+ .pro6pp-input-addons {
419
+ position: absolute;
420
+ right: 4px;
421
+ top: 0;
422
+ bottom: 0;
423
+ display: flex;
424
+ align-items: center;
425
+ pointer-events: none;
426
+ }
427
+ .pro6pp-input-addons > * {
428
+ pointer-events: auto;
429
+ }
430
+
431
+ .pro6pp-clear-button {
432
+ background: none;
433
+ border: none;
434
+ width: 40px;
435
+ height: 40px;
436
+ cursor: pointer;
437
+ color: #a3a3a3;
438
+ display: flex;
439
+ align-items: center;
440
+ justify-content: center;
441
+ border-radius: 50%;
442
+ transition: color 0.2s, background-color 0.2s;
443
+ touch-action: manipulation;
444
+ }
445
+
446
+ @media (hover: hover) {
447
+ .pro6pp-clear-button:hover {
448
+ color: #1f2937;
449
+ background-color: #f3f4f6;
450
+ }
451
+ }
452
+
453
+ .pro6pp-clear-button:active {
454
+ background-color: #f3f4f6;
455
+ }
456
+
457
+ .pro6pp-loader {
458
+ width: 20px;
459
+ height: 20px;
460
+ margin: 0 8px;
461
+ border: 2px solid #e0e0e0;
462
+ border-top-color: #6b7280;
463
+ border-radius: 50%;
464
+ animation: pro6pp-spin 0.6s linear infinite;
465
+ flex-shrink: 0;
466
+ }
467
+
468
+ .pro6pp-dropdown {
469
+ position: absolute;
470
+ top: 100%;
471
+ left: 0;
472
+ right: 0;
473
+ margin-top: 4px;
474
+ background: #ffffff;
475
+ border: 1px solid #e5e7eb;
476
+ border-radius: 6px;
477
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
478
+ z-index: 9999;
479
+ padding: 0;
480
+ max-height: 260px;
481
+ overflow-y: auto;
482
+ display: flex;
483
+ flex-direction: column;
484
+ }
485
+
486
+ @media (max-height: 500px) {
487
+ .pro6pp-dropdown {
488
+ max-height: 140px;
489
+ }
490
+ }
491
+
492
+ .pro6pp-list {
493
+ list-style: none;
494
+ margin: 0;
495
+ padding: 0;
496
+ width: 100%;
497
+ }
498
+
499
+ .pro6pp-item {
500
+ padding: 12px 14px;
501
+ cursor: pointer;
502
+ display: flex;
503
+ align-items: center;
504
+ font-size: 14px;
505
+ color: #374151;
506
+ border-bottom: 1px solid #f3f4f6;
507
+ transition: background-color 0.1s;
508
+ flex-shrink: 0;
509
+ }
510
+
511
+ .pro6pp-item:last-child {
512
+ border-bottom: none;
15
513
  }
16
- return to;
514
+
515
+ @media (hover: hover) {
516
+ .pro6pp-item:hover, .pro6pp-item--active {
517
+ background-color: #f9fafb;
518
+ }
519
+ }
520
+
521
+ .pro6pp-item:active {
522
+ background-color: #f3f4f6;
523
+ }
524
+
525
+ .pro6pp-item__label {
526
+ font-weight: 500;
527
+ flex-shrink: 0;
528
+ }
529
+ .pro6pp-item__subtitle {
530
+ font-size: 14px;
531
+ color: #6b7280;
532
+ flex-grow: 1;
533
+ }
534
+ .pro6pp-item__chevron {
535
+ color: #d1d5db;
536
+ display: flex;
537
+ align-items: center;
538
+ margin-left: auto;
539
+ }
540
+
541
+ .pro6pp-no-results {
542
+ padding: 24px 16px;
543
+ color: #6b7280;
544
+ font-size: 15px;
545
+ text-align: center;
546
+ }
547
+
548
+ .pro6pp-load-more {
549
+ width: 100%;
550
+ padding: 14px;
551
+ background: #f9fafb;
552
+ border: none;
553
+ border-top: 1px solid #e0e0e0;
554
+ color: #3b82f6;
555
+ font-size: 14px;
556
+ font-weight: 600;
557
+ cursor: pointer;
558
+ flex-shrink: 0;
559
+ touch-action: manipulation;
560
+ }
561
+
562
+ .pro6pp-load-more:active {
563
+ background-color: #f3f4f6;
564
+ }
565
+
566
+ @keyframes pro6pp-spin {
567
+ to { transform: rotate(360deg); }
568
+ }
569
+ `;
570
+
571
+ // src/index.tsx
572
+ var HighlightedText = ({ text, query }) => {
573
+ const segments = useMemo(() => getHighlightSegments(text, query), [text, query]);
574
+ return /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__label" }, segments.map(
575
+ (seg, i) => seg.match ? /* @__PURE__ */ React.createElement("strong", { key: i, className: "pro6pp-item__label--match" }, seg.text) : seg.text
576
+ ));
17
577
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- useInfer: () => useInfer
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
- var import_react = require("react");
27
- var import_infer_core = require("@pro6pp/infer-core");
28
578
  function useInfer(config) {
29
- const [state, setState] = (0, import_react.useState)(import_infer_core.INITIAL_STATE);
30
- const core = (0, import_react.useMemo)(() => {
31
- return new import_infer_core.InferCore({
579
+ const [state, setState] = useState(INITIAL_STATE);
580
+ const callbacksRef = useRef({
581
+ onStateChange: config.onStateChange,
582
+ onSelect: config.onSelect
583
+ });
584
+ useEffect(() => {
585
+ callbacksRef.current = {
586
+ onStateChange: config.onStateChange,
587
+ onSelect: config.onSelect
588
+ };
589
+ }, [config.onStateChange, config.onSelect]);
590
+ const core = useMemo(() => {
591
+ return new InferCore({
32
592
  ...config,
33
- onStateChange: (newState) => setState({ ...newState })
593
+ onStateChange: (newState) => {
594
+ setState({ ...newState });
595
+ callbacksRef.current.onStateChange?.(newState);
596
+ },
597
+ onSelect: (selection) => {
598
+ callbacksRef.current.onSelect?.(selection);
599
+ }
34
600
  });
35
- }, [config.country, config.authKey, config.limit]);
601
+ }, [
602
+ config.country,
603
+ config.authKey,
604
+ config.apiUrl,
605
+ config.fetcher,
606
+ config.limit,
607
+ config.debounceMs,
608
+ config.maxRetries
609
+ ]);
36
610
  return {
611
+ /** The current UI state (suggestions, loading status, query, etc.). */
37
612
  state,
613
+ /** The raw InferCore instance for manual control. */
38
614
  core,
615
+ /** Pre-configured event handlers to spread onto an <input /> element. */
39
616
  inputProps: {
40
617
  value: state.query,
41
618
  onChange: (e) => core.handleInput(e.target.value),
42
619
  onKeyDown: (e) => core.handleKeyDown(e)
43
620
  },
44
- selectItem: (item) => core.selectItem(item)
621
+ /** Function to manually select a specific suggestion. */
622
+ selectItem: (item) => core.selectItem(item),
623
+ /** Function to load more results. */
624
+ loadMore: () => core.loadMore()
45
625
  };
46
626
  }
47
- // Annotate the CommonJS export names for ESM import in node:
48
- 0 && (module.exports = {
627
+ var Pro6PPInfer = forwardRef(
628
+ ({
629
+ className,
630
+ style,
631
+ inputProps,
632
+ placeholder = "Start typing an address...",
633
+ renderItem,
634
+ disableDefaultStyles = false,
635
+ noResultsText = "No results found",
636
+ loadMoreText = "Show more results...",
637
+ renderNoResults,
638
+ showClearButton = true,
639
+ ...config
640
+ }, ref) => {
641
+ const { state, selectItem, loadMore, inputProps: coreInputProps, core } = useInfer(config);
642
+ const [isOpen, setIsOpen] = useState(false);
643
+ const internalInputRef = useRef(null);
644
+ const wrapperRef = useRef(null);
645
+ useImperativeHandle(ref, () => internalInputRef.current);
646
+ useEffect(() => {
647
+ if (disableDefaultStyles) return;
648
+ const styleId = "pro6pp-styles";
649
+ if (!document.getElementById(styleId)) {
650
+ const styleEl = document.createElement("style");
651
+ styleEl.id = styleId;
652
+ styleEl.textContent = DEFAULT_STYLES;
653
+ document.head.appendChild(styleEl);
654
+ }
655
+ }, [disableDefaultStyles]);
656
+ useEffect(() => {
657
+ const handleClickOutside = (event) => {
658
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
659
+ setIsOpen(false);
660
+ }
661
+ };
662
+ document.addEventListener("mousedown", handleClickOutside);
663
+ return () => document.removeEventListener("mousedown", handleClickOutside);
664
+ }, []);
665
+ const items = useMemo(() => {
666
+ return [
667
+ ...state.cities.map((c) => ({ ...c, type: "city" })),
668
+ ...state.streets.map((s) => ({ ...s, type: "street" })),
669
+ ...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
670
+ ];
671
+ }, [state.cities, state.streets, state.suggestions]);
672
+ const handleSelect = (item) => {
673
+ const isFinal = selectItem(item);
674
+ if (!isFinal) {
675
+ setTimeout(() => internalInputRef.current?.focus(), 0);
676
+ } else {
677
+ setIsOpen(false);
678
+ }
679
+ };
680
+ const handleClear = () => {
681
+ core.handleInput("");
682
+ internalInputRef.current?.focus();
683
+ };
684
+ const hasResults = items.length > 0;
685
+ const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
686
+ const showDropdown = isOpen && (hasResults || state.isLoading || showNoResults);
687
+ return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, className: `pro6pp-wrapper ${className || ""}`, style }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(
688
+ "input",
689
+ {
690
+ ref: internalInputRef,
691
+ type: "text",
692
+ className: "pro6pp-input",
693
+ placeholder,
694
+ autoComplete: "off",
695
+ autoCorrect: "off",
696
+ autoCapitalize: "none",
697
+ spellCheck: "false",
698
+ inputMode: "search",
699
+ enterKeyHint: "search",
700
+ ...inputProps,
701
+ ...coreInputProps,
702
+ onFocus: (e) => {
703
+ setIsOpen(true);
704
+ inputProps?.onFocus?.(e);
705
+ }
706
+ }
707
+ ), /* @__PURE__ */ React.createElement("div", { className: "pro6pp-input-addons" }, state.isLoading && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-loader" }), showClearButton && state.query.length > 0 && /* @__PURE__ */ React.createElement(
708
+ "button",
709
+ {
710
+ type: "button",
711
+ className: "pro6pp-clear-button",
712
+ onClick: handleClear,
713
+ "aria-label": "Clear input"
714
+ },
715
+ /* @__PURE__ */ React.createElement(
716
+ "svg",
717
+ {
718
+ width: "14",
719
+ height: "14",
720
+ viewBox: "0 0 24 24",
721
+ fill: "none",
722
+ stroke: "currentColor",
723
+ strokeWidth: "2",
724
+ strokeLinecap: "round",
725
+ strokeLinejoin: "round"
726
+ },
727
+ /* @__PURE__ */ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
728
+ /* @__PURE__ */ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
729
+ )
730
+ ))), showDropdown && /* @__PURE__ */ React.createElement(
731
+ "div",
732
+ {
733
+ className: "pro6pp-dropdown",
734
+ onWheel: (e) => e.stopPropagation(),
735
+ onMouseDown: (e) => e.stopPropagation()
736
+ },
737
+ /* @__PURE__ */ React.createElement("ul", { className: "pro6pp-list", role: "listbox" }, hasResults ? items.map((item, index) => {
738
+ const isActive = index === state.selectedSuggestionIndex;
739
+ const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
740
+ const showChevron = item.value === void 0 || item.value === null;
741
+ return /* @__PURE__ */ React.createElement(
742
+ "li",
743
+ {
744
+ key: `${item.label}-${index}`,
745
+ role: "option",
746
+ "aria-selected": isActive,
747
+ className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
748
+ onMouseDown: (e) => e.preventDefault(),
749
+ onClick: () => handleSelect(item)
750
+ },
751
+ renderItem ? renderItem(item, isActive) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(HighlightedText, { text: item.label, query: state.query }), secondaryText && /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__subtitle" }, ", ", secondaryText), showChevron && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-item__chevron" }, /* @__PURE__ */ React.createElement(
752
+ "svg",
753
+ {
754
+ width: "16",
755
+ height: "16",
756
+ viewBox: "0 0 24 24",
757
+ fill: "none",
758
+ stroke: "currentColor",
759
+ strokeWidth: "2",
760
+ strokeLinecap: "round",
761
+ strokeLinejoin: "round"
762
+ },
763
+ /* @__PURE__ */ React.createElement("polyline", { points: "9 18 15 12 9 6" })
764
+ )))
765
+ );
766
+ }) : state.isLoading ? /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, "Loading suggestions...") : /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)),
767
+ state.hasMore && /* @__PURE__ */ React.createElement(
768
+ "button",
769
+ {
770
+ type: "button",
771
+ className: "pro6pp-load-more",
772
+ onClick: (e) => {
773
+ e.preventDefault();
774
+ loadMore();
775
+ }
776
+ },
777
+ loadMoreText
778
+ )
779
+ ));
780
+ }
781
+ );
782
+ export {
783
+ Pro6PPInfer,
49
784
  useInfer
50
- });
785
+ };