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