@pro6pp/infer-react 0.0.2-beta.9 → 0.1.0-beta.19

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
@@ -3,7 +3,108 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.tsx
6
- import React, { useState, useMemo, useEffect, useRef } from "react";
6
+ import React, {
7
+ useState,
8
+ useMemo,
9
+ useEffect,
10
+ useRef,
11
+ forwardRef,
12
+ useImperativeHandle
13
+ } from "react";
14
+
15
+ // ../core/src/label-formatter.ts
16
+ function normalize(str) {
17
+ return str.toLowerCase().trim();
18
+ }
19
+ function escapeRegex(str) {
20
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21
+ }
22
+ function findWordPosition(query, value) {
23
+ const normalizedQuery = normalize(query);
24
+ const normalizedValue = normalize(value);
25
+ if (normalizedValue.includes(" ")) {
26
+ return normalizedQuery.indexOf(normalizedValue);
27
+ }
28
+ const pattern = new RegExp(`(?:^|[,\\s])${escapeRegex(normalizedValue)}(?:$|[,\\s])`, "g");
29
+ const match = pattern.exec(normalizedQuery);
30
+ if (match) {
31
+ const matchStart = match.index;
32
+ const firstChar = normalizedQuery[matchStart];
33
+ if (firstChar === "," || firstChar === " ") {
34
+ return matchStart + 1;
35
+ }
36
+ return matchStart;
37
+ }
38
+ return -1;
39
+ }
40
+ function detectComponentOrder(query, value) {
41
+ const detected = [];
42
+ const componentMap = [];
43
+ if (value.street) {
44
+ componentMap.push({ value: value.street, type: "street" });
45
+ }
46
+ if (value.city) {
47
+ componentMap.push({ value: value.city, type: "city" });
48
+ }
49
+ if (value.postcode) {
50
+ componentMap.push({ value: value.postcode, type: "postcode" });
51
+ }
52
+ if (value.street_number !== void 0 && value.street_number !== null) {
53
+ componentMap.push({ value: String(value.street_number), type: "street_number" });
54
+ }
55
+ if (value.addition) {
56
+ componentMap.push({ value: value.addition, type: "addition" });
57
+ }
58
+ for (const comp of componentMap) {
59
+ const position = findWordPosition(query, comp.value);
60
+ if (position !== -1) {
61
+ detected.push({
62
+ type: comp.type,
63
+ value: comp.value,
64
+ position
65
+ });
66
+ }
67
+ }
68
+ detected.sort((a, b) => a.position - b.position);
69
+ return detected;
70
+ }
71
+ function formatLabelByInputOrder(query, value) {
72
+ if (!value || !query) {
73
+ return "";
74
+ }
75
+ const detectedOrder = detectComponentOrder(query, value);
76
+ const detectedTypes = new Set(detectedOrder.map((d) => d.type));
77
+ const parts = [];
78
+ for (const detected of detectedOrder) {
79
+ parts.push(detected.value);
80
+ }
81
+ const defaultOrder = ["street", "street_number", "addition", "postcode", "city"];
82
+ for (const type of defaultOrder) {
83
+ if (detectedTypes.has(type)) continue;
84
+ let val;
85
+ switch (type) {
86
+ case "street":
87
+ val = value.street;
88
+ break;
89
+ case "city":
90
+ val = value.city;
91
+ break;
92
+ case "street_number":
93
+ val = value.street_number !== void 0 ? String(value.street_number) : void 0;
94
+ break;
95
+ case "postcode":
96
+ val = value.postcode;
97
+ break;
98
+ case "addition":
99
+ val = value.addition;
100
+ break;
101
+ }
102
+ if (val) {
103
+ parts.push(val);
104
+ }
105
+ }
106
+ return parts.join(", ");
107
+ }
7
108
 
8
109
  // ../core/src/core.ts
9
110
  var DEFAULTS = {
@@ -14,7 +115,8 @@ var DEFAULTS = {
14
115
  MAX_RETRIES: 0
15
116
  };
16
117
  var PATTERNS = {
17
- DIGITS_1_3: /^[0-9]{1,3}$/
118
+ DIGITS_1_3: /^[0-9]{1,3}$/,
119
+ STREET_NUMBER_PREFIX: /^(\d+)\s*,\s*$/
18
120
  };
19
121
  var INITIAL_STATE = {
20
122
  query: "",
@@ -23,6 +125,7 @@ var INITIAL_STATE = {
23
125
  streets: [],
24
126
  suggestions: [],
25
127
  isValid: false,
128
+ value: null,
26
129
  isError: false,
27
130
  isLoading: false,
28
131
  hasMore: false,
@@ -36,10 +139,11 @@ var InferCore = class {
36
139
  constructor(config) {
37
140
  __publicField(this, "country");
38
141
  __publicField(this, "authKey");
39
- __publicField(this, "apiUrl");
142
+ __publicField(this, "explicitApiUrl");
40
143
  __publicField(this, "baseLimit");
41
144
  __publicField(this, "currentLimit");
42
145
  __publicField(this, "maxRetries");
146
+ __publicField(this, "language");
43
147
  __publicField(this, "fetcher");
44
148
  __publicField(this, "onStateChange");
45
149
  __publicField(this, "onSelect");
@@ -50,12 +154,12 @@ var InferCore = class {
50
154
  __publicField(this, "state");
51
155
  __publicField(this, "abortController", null);
52
156
  __publicField(this, "debouncedFetch");
53
- __publicField(this, "isSelecting", false);
54
157
  this.country = config.country;
55
158
  this.authKey = config.authKey;
56
- this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
159
+ this.explicitApiUrl = config.apiUrl;
57
160
  this.baseLimit = config.limit || DEFAULTS.LIMIT;
58
161
  this.currentLimit = this.baseLimit;
162
+ this.language = config.language;
59
163
  const configRetries = config.maxRetries !== void 0 ? config.maxRetries : DEFAULTS.MAX_RETRIES;
60
164
  this.maxRetries = Math.max(0, Math.min(configRetries, 10));
61
165
  this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
@@ -74,18 +178,16 @@ var InferCore = class {
74
178
  * @param value The raw string from the input field.
75
179
  */
76
180
  handleInput(value) {
77
- if (this.isSelecting) {
78
- this.isSelecting = false;
79
- return;
80
- }
81
181
  this.currentLimit = this.baseLimit;
82
182
  const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
83
183
  this.updateState({
84
184
  query: value,
85
185
  isValid: false,
186
+ value: null,
86
187
  isLoading: !!value.trim(),
87
188
  selectedSuggestionIndex: -1,
88
- hasMore: false
189
+ hasMore: false,
190
+ stage: isEditingFinal ? null : this.state.stage
89
191
  });
90
192
  if (isEditingFinal) {
91
193
  this.onSelect(null);
@@ -106,7 +208,7 @@ var InferCore = class {
106
208
  * Supports:
107
209
  * - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
108
210
  * - `Enter`: Select the currently highlighted suggestion.
109
- * - `Space`: Automatically inserts a comma if a numeric house number is detected.
211
+ * - `Space`: Automatically inserts a comma if a numeric street number is detected.
110
212
  * @param event The keyboard event from the input element.
111
213
  */
112
214
  handleKeyDown(event) {
@@ -154,6 +256,7 @@ var InferCore = class {
154
256
  * Manually selects a suggestion or a string value.
155
257
  * This is typically called when a user clicks a suggestion in the UI.
156
258
  * @param item The suggestion object or string to select.
259
+ * @returns boolean True if the selection is a final address.
157
260
  */
158
261
  selectItem(item) {
159
262
  this.debouncedFetch.cancel();
@@ -167,26 +270,27 @@ var InferCore = class {
167
270
  }
168
271
  const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
169
272
  const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
170
- this.isSelecting = true;
171
273
  if (this.state.stage === "final" || isFullResult) {
172
274
  let finalQuery = label;
173
275
  if (valueObj && Object.keys(valueObj).length > 0) {
174
- const { street, street_number, house_number, city } = valueObj;
175
- const number = street_number || house_number;
176
- if (street && number && city) {
177
- finalQuery = `${street} ${number}, ${city}`;
276
+ const { street, street_number, postcode, city, addition } = valueObj;
277
+ if (street && street_number && city) {
278
+ const suffix = addition ? ` ${addition}` : "";
279
+ const postcodeStr = postcode ? `${postcode}, ` : "";
280
+ finalQuery = `${street}, ${street_number}${suffix}, ${postcodeStr}${city}`;
178
281
  }
179
282
  }
180
283
  this.finishSelection(finalQuery, valueObj);
181
- return;
284
+ return true;
182
285
  }
183
286
  const subtitle = typeof item !== "string" ? item.subtitle : null;
184
287
  this.processSelection(logicValue, subtitle);
288
+ return false;
185
289
  }
186
290
  shouldAutoInsertComma(currentVal) {
187
291
  const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
188
292
  if (isStartOfSegmentAndNumeric) return true;
189
- if (this.state.stage === "house_number") {
293
+ if (this.state.stage === "street_number") {
190
294
  const currentFragment = this.getCurrentFragment(currentVal);
191
295
  return PATTERNS.DIGITS_1_3.test(currentFragment);
192
296
  }
@@ -199,13 +303,11 @@ var InferCore = class {
199
303
  cities: [],
200
304
  streets: [],
201
305
  isValid: true,
306
+ value: value || null,
202
307
  stage: "final",
203
308
  hasMore: false
204
309
  });
205
310
  this.onSelect(value || label);
206
- setTimeout(() => {
207
- this.isSelecting = false;
208
- }, 0);
209
311
  }
210
312
  processSelection(text, subtitle) {
211
313
  const { stage, query } = this.state;
@@ -216,7 +318,22 @@ var InferCore = class {
216
318
  nextQuery = `${subtitle}, ${text}, `;
217
319
  } else {
218
320
  const prefix = this.getQueryPrefix(query);
219
- nextQuery = prefix ? `${prefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
321
+ const shouldAddSubtitle = !prefix || !prefix.includes(subtitle);
322
+ let effectivePrefix = prefix;
323
+ if (prefix && subtitle) {
324
+ const prefixNumMatch = prefix.match(PATTERNS.STREET_NUMBER_PREFIX);
325
+ if (prefixNumMatch) {
326
+ const num = prefixNumMatch[1];
327
+ if (subtitle.startsWith(num)) {
328
+ effectivePrefix = "";
329
+ }
330
+ }
331
+ }
332
+ if (shouldAddSubtitle) {
333
+ nextQuery = effectivePrefix ? `${effectivePrefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
334
+ } else {
335
+ nextQuery = effectivePrefix ? `${effectivePrefix} ${text}, ` : `${text}, `;
336
+ }
220
337
  }
221
338
  this.updateQueryAndFetch(nextQuery);
222
339
  return;
@@ -226,12 +343,12 @@ var InferCore = class {
226
343
  return;
227
344
  }
228
345
  const hasComma = query.includes(",");
229
- const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
346
+ const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "street_number_first");
230
347
  if (isFirstSegment) {
231
348
  nextQuery = `${text}, `;
232
349
  } else {
233
350
  nextQuery = this.replaceLastSegment(query, text);
234
- if (stage !== "house_number") {
351
+ if (stage !== "street_number") {
235
352
  nextQuery += ", ";
236
353
  }
237
354
  }
@@ -250,14 +367,23 @@ var InferCore = class {
250
367
  this.abortController = new AbortController();
251
368
  }
252
369
  const currentSignal = this.abortController?.signal;
253
- const url = new URL(`${this.apiUrl}/infer/${this.country.toLowerCase()}`);
254
- const params = {
255
- authKey: this.authKey,
370
+ const baseUrl = this.explicitApiUrl ? this.explicitApiUrl : `${DEFAULTS.API_URL}/infer/${this.country.toLowerCase()}`;
371
+ const params = new URLSearchParams({
256
372
  query: text,
257
373
  limit: this.currentLimit.toString()
258
- };
259
- url.search = new URLSearchParams(params).toString();
260
- this.fetcher(url.toString(), { signal: currentSignal }).then((res) => {
374
+ });
375
+ if (this.explicitApiUrl) {
376
+ params.append("country", this.country.toLowerCase());
377
+ }
378
+ if (this.authKey) {
379
+ params.set("authKey", this.authKey);
380
+ }
381
+ if (this.language) {
382
+ params.set("language", this.language);
383
+ }
384
+ const separator = baseUrl.includes("?") ? "&" : "?";
385
+ const finalUrl = `${baseUrl}${separator}${params.toString()}`;
386
+ this.fetcher(finalUrl, { signal: currentSignal }).then((res) => {
261
387
  if (!res.ok) {
262
388
  if (attempt < this.maxRetries && (res.status >= 500 || res.status === 429)) {
263
389
  return this.retry(val, attempt, currentSignal);
@@ -289,8 +415,6 @@ var InferCore = class {
289
415
  stage: data.stage,
290
416
  isLoading: false
291
417
  };
292
- let autoSelect = false;
293
- let autoSelectItem = null;
294
418
  const rawSuggestions = data.suggestions || [];
295
419
  const uniqueSuggestions = [];
296
420
  const seen = /* @__PURE__ */ new Set();
@@ -298,7 +422,8 @@ var InferCore = class {
298
422
  const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
299
423
  if (!seen.has(key)) {
300
424
  seen.add(key);
301
- uniqueSuggestions.push(item);
425
+ const reformattedItem = this.reformatSuggestionLabel(item);
426
+ uniqueSuggestions.push(reformattedItem);
302
427
  }
303
428
  }
304
429
  const totalCount = uniqueSuggestions.length + (data.cities?.length || 0) + (data.streets?.length || 0);
@@ -306,38 +431,53 @@ var InferCore = class {
306
431
  if (data.stage === "mixed") {
307
432
  newState.cities = data.cities || [];
308
433
  newState.streets = data.streets || [];
309
- newState.suggestions = [];
434
+ if (newState.cities?.length === 0 && newState.streets?.length === 0) {
435
+ newState.suggestions = uniqueSuggestions;
436
+ } else {
437
+ newState.suggestions = [];
438
+ }
310
439
  } else {
311
440
  newState.suggestions = uniqueSuggestions;
312
441
  newState.cities = [];
313
442
  newState.streets = [];
314
- if (data.stage === "final" && uniqueSuggestions.length === 1) {
315
- autoSelect = true;
316
- autoSelectItem = uniqueSuggestions[0];
317
- }
318
443
  }
319
444
  newState.isValid = data.stage === "final";
320
- if (autoSelect && autoSelectItem) {
321
- newState.query = autoSelectItem.label;
322
- newState.suggestions = [];
323
- newState.cities = [];
324
- newState.streets = [];
325
- newState.isValid = true;
326
- newState.hasMore = false;
327
- this.updateState(newState);
328
- const val = typeof autoSelectItem.value === "object" ? autoSelectItem.value : autoSelectItem.label;
329
- this.onSelect(val);
330
- } else {
331
- this.updateState(newState);
445
+ this.updateState(newState);
446
+ if (newState.isValid && uniqueSuggestions.length === 1) {
447
+ this.selectItem(uniqueSuggestions[0]);
332
448
  }
333
449
  }
450
+ /**
451
+ * Reformats a suggestion's label based on the user's input order.
452
+ * If the suggestion has a structured value object, we reorder the label
453
+ * to match how the user typed the components.
454
+ */
455
+ reformatSuggestionLabel(item) {
456
+ if (!item.value || typeof item.value === "string") {
457
+ return item;
458
+ }
459
+ const addressValue = item.value;
460
+ if (!addressValue.street || !addressValue.city) {
461
+ return item;
462
+ }
463
+ const reformattedLabel = formatLabelByInputOrder(this.state.query, addressValue);
464
+ if (reformattedLabel) {
465
+ return { ...item, label: reformattedLabel };
466
+ }
467
+ return item;
468
+ }
334
469
  updateQueryAndFetch(nextQuery) {
335
- this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
336
- this.updateState({ isLoading: true, isValid: false, hasMore: false });
470
+ this.updateState({
471
+ query: nextQuery,
472
+ suggestions: [],
473
+ cities: [],
474
+ streets: [],
475
+ isValid: false,
476
+ value: null,
477
+ isLoading: true,
478
+ hasMore: false
479
+ });
337
480
  this.debouncedFetch(nextQuery);
338
- setTimeout(() => {
339
- this.isSelecting = false;
340
- }, 0);
341
481
  }
342
482
  replaceLastSegment(fullText, newSegment) {
343
483
  const lastCommaIndex = fullText.lastIndexOf(",");
@@ -374,6 +514,49 @@ var InferCore = class {
374
514
  }
375
515
  };
376
516
 
517
+ // ../core/src/highlight.ts
518
+ function mergeSegments(segments) {
519
+ if (segments.length === 0) return segments;
520
+ const merged = [];
521
+ for (const seg of segments) {
522
+ const last = merged[merged.length - 1];
523
+ if (last && last.match === seg.match) {
524
+ last.text += seg.text;
525
+ } else {
526
+ merged.push({ text: seg.text, match: seg.match });
527
+ }
528
+ }
529
+ return merged;
530
+ }
531
+ function getHighlightSegments(text, query) {
532
+ if (!query || !text) return [{ text, match: false }];
533
+ const segments = [];
534
+ const normalizedText = text.toLowerCase();
535
+ const normalizedQuery = query.toLowerCase();
536
+ let queryCursor = 0;
537
+ let unmatchedCursor = 0;
538
+ for (let textCursor = 0; textCursor < text.length; textCursor++) {
539
+ const isMatch = queryCursor < query.length && normalizedText[textCursor] === normalizedQuery[queryCursor];
540
+ if (!isMatch) continue;
541
+ const hasPrecedingUnmatched = textCursor > unmatchedCursor;
542
+ if (hasPrecedingUnmatched) {
543
+ segments.push({ text: text.slice(unmatchedCursor, textCursor), match: false });
544
+ }
545
+ segments.push({ text: text[textCursor], match: true });
546
+ queryCursor++;
547
+ unmatchedCursor = textCursor + 1;
548
+ }
549
+ const hasRemainingText = unmatchedCursor < text.length;
550
+ if (hasRemainingText) {
551
+ segments.push({ text: text.slice(unmatchedCursor), match: false });
552
+ }
553
+ const isFullMatch = queryCursor === query.length;
554
+ if (!isFullMatch) {
555
+ return [{ text, match: false }];
556
+ }
557
+ return mergeSegments(segments);
558
+ }
559
+
377
560
  // ../core/src/default-styles.ts
378
561
  var DEFAULT_STYLES = `
379
562
  .pro6pp-wrapper {
@@ -381,20 +564,28 @@ var DEFAULT_STYLES = `
381
564
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
382
565
  box-sizing: border-box;
383
566
  width: 100%;
567
+ -webkit-tap-highlight-color: transparent;
384
568
  }
385
569
  .pro6pp-wrapper * {
386
570
  box-sizing: border-box;
387
571
  }
388
572
  .pro6pp-input {
389
573
  width: 100%;
390
- padding: 10px 12px;
574
+ padding: 12px 14px;
391
575
  padding-right: 48px;
392
576
  border: 1px solid #e0e0e0;
393
- border-radius: 4px;
577
+ border-radius: 8px;
394
578
  font-size: 16px;
395
579
  line-height: 1.5;
580
+ appearance: none;
396
581
  transition: border-color 0.2s, box-shadow 0.2s;
397
582
  }
583
+
584
+ .pro6pp-input::placeholder {
585
+ font-size: 16px;
586
+ color: #a3a3a3;
587
+ }
588
+
398
589
  .pro6pp-input:focus {
399
590
  outline: none;
400
591
  border-color: #3b82f6;
@@ -403,12 +594,11 @@ var DEFAULT_STYLES = `
403
594
 
404
595
  .pro6pp-input-addons {
405
596
  position: absolute;
406
- right: 6px;
597
+ right: 4px;
407
598
  top: 0;
408
599
  bottom: 0;
409
600
  display: flex;
410
601
  align-items: center;
411
- gap: 2px;
412
602
  pointer-events: none;
413
603
  }
414
604
  .pro6pp-input-addons > * {
@@ -418,37 +608,27 @@ var DEFAULT_STYLES = `
418
608
  .pro6pp-clear-button {
419
609
  background: none;
420
610
  border: none;
421
- width: 28px;
422
- height: 28px;
611
+ width: 32px;
612
+ height: 32px;
423
613
  cursor: pointer;
424
614
  color: #a3a3a3;
425
615
  display: flex;
426
616
  align-items: center;
427
617
  justify-content: center;
428
618
  border-radius: 50%;
429
- transition: color 0.2s, background-color 0.2s, transform 0.1s;
619
+ transition: color 0.2s, background-color 0.2s;
620
+ touch-action: manipulation;
430
621
  }
431
- .pro6pp-clear-button:hover {
432
- color: #1f2937;
433
- background-color: #f3f4f6;
434
- }
435
- .pro6pp-clear-button:active {
436
- transform: scale(0.92);
437
- }
438
- .pro6pp-clear-button svg {
439
- width: 18px;
440
- height: 18px;
622
+
623
+ @media (hover: hover) {
624
+ .pro6pp-clear-button:hover {
625
+ color: #1f2937;
626
+ background-color: #f3f4f6;
627
+ }
441
628
  }
442
629
 
443
- .pro6pp-loader {
444
- width: 18px;
445
- height: 18px;
446
- margin: 0 4px;
447
- border: 2px solid #e0e0e0;
448
- border-top-color: #6b7280;
449
- border-radius: 50%;
450
- animation: pro6pp-spin 0.6s linear infinite;
451
- flex-shrink: 0;
630
+ .pro6pp-clear-button:active {
631
+ background-color: #f3f4f6;
452
632
  }
453
633
 
454
634
  .pro6pp-dropdown {
@@ -456,77 +636,126 @@ var DEFAULT_STYLES = `
456
636
  top: 100%;
457
637
  left: 0;
458
638
  right: 0;
459
- z-index: 9999;
460
639
  margin-top: 4px;
461
- background: white;
462
- border: 1px solid #e0e0e0;
463
- border-radius: 4px;
464
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
465
- max-height: 300px;
640
+ background: #ffffff;
641
+ border: 1px solid #e5e7eb;
642
+ border-radius: 6px;
643
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
644
+ z-index: 9999;
645
+ padding: 0;
646
+ max-height: 280px;
466
647
  overflow-y: auto;
467
648
  display: flex;
468
649
  flex-direction: column;
469
650
  }
651
+
652
+ @media (max-height: 500px) {
653
+ .pro6pp-dropdown {
654
+ max-height: 180px;
655
+ }
656
+ }
657
+
470
658
  .pro6pp-list {
471
- list-style: none !important;
472
- padding: 0 !important;
473
- margin: 0 !important;
474
- flex-grow: 1;
659
+ list-style: none;
660
+ margin: 0;
661
+ padding: 0;
662
+ width: 100%;
475
663
  }
664
+
476
665
  .pro6pp-item {
477
- padding: 12px 16px;
666
+ padding: 12px 14px;
478
667
  cursor: pointer;
479
668
  display: flex;
480
- flex-direction: row;
481
669
  align-items: center;
482
- color: #111827;
483
- font-size: 14px;
484
- line-height: 1.2;
485
- white-space: nowrap;
486
- overflow: hidden;
670
+ font-size: 15px;
671
+ line-height: 1.4;
672
+ color: #374151;
673
+ border-bottom: 1px solid #f3f4f6;
674
+ transition: background-color 0.1s;
675
+ flex-shrink: 0;
487
676
  }
488
- .pro6pp-item:hover, .pro6pp-item--active {
489
- background-color: #f9fafb;
677
+
678
+ .pro6pp-item:last-child {
679
+ border-bottom: none;
680
+ }
681
+
682
+ @media (hover: hover) {
683
+ .pro6pp-item:hover, .pro6pp-item--active {
684
+ background-color: #f9fafb;
685
+ }
490
686
  }
687
+
688
+ .pro6pp-item:active {
689
+ background-color: #f3f4f6;
690
+ }
691
+
491
692
  .pro6pp-item__label {
492
- font-weight: 500;
493
- flex-shrink: 0;
693
+ font-weight: 400;
694
+ flex-shrink: 1;
695
+ overflow: hidden;
696
+ text-overflow: ellipsis;
697
+ white-space: nowrap;
494
698
  }
699
+
700
+ .pro6pp-item__label--match {
701
+ font-weight: 520;
702
+ }
703
+
704
+ .pro6pp-item__label--unmatched {
705
+ font-weight: 400;
706
+ color: #4b5563;
707
+ }
708
+
495
709
  .pro6pp-item__subtitle {
496
- font-size: 14px;
497
710
  color: #6b7280;
498
- overflow: hidden;
499
- text-overflow: ellipsis;
500
- flex-shrink: 1;
711
+ flex-shrink: 0;
501
712
  }
713
+
502
714
  .pro6pp-item__chevron {
503
- margin-left: auto;
715
+ color: #d1d5db;
504
716
  display: flex;
505
717
  align-items: center;
506
- color: #9ca3af;
718
+ margin-left: auto;
507
719
  padding-left: 8px;
508
720
  }
721
+
509
722
  .pro6pp-no-results {
510
- padding: 16px;
723
+ padding: 24px 16px;
511
724
  color: #6b7280;
512
- font-size: 14px;
725
+ font-size: 15px;
513
726
  text-align: center;
514
727
  }
515
- .pro6pp-load-more {
516
- width: 100%;
517
- padding: 10px;
518
- background: #f9fafb;
519
- border: none;
520
- border-top: 1px solid #e0e0e0;
521
- color: #3b82f6;
522
- font-size: 13px;
523
- font-weight: 600;
524
- cursor: pointer;
525
- transition: background-color 0.2s;
526
- flex-shrink: 0;
728
+
729
+ .pro6pp-loader-item {
730
+ padding: 10px 12px;
731
+ color: #6b7280;
732
+ font-size: 0.875rem;
733
+ display: flex;
734
+ align-items: center;
735
+ justify-content: center;
736
+ gap: 8px;
737
+ background-color: #f9fafb;
738
+ border-top: 1px solid #f3f4f6;
527
739
  }
528
- .pro6pp-load-more:hover {
529
- background-color: #f3f4f6;
740
+
741
+ .pro6pp-mini-spinner {
742
+ width: 14px;
743
+ height: 14px;
744
+ border: 2px solid #e5e7eb;
745
+ border-top-color: #6b7280;
746
+ border-radius: 50%;
747
+ animation: pro6pp-spin 0.6s linear infinite;
748
+ }
749
+
750
+ @media (max-width: 640px) {
751
+ .pro6pp-input {
752
+ font-size: 16px;
753
+ padding: 10px 12px;
754
+ }
755
+ .pro6pp-item {
756
+ padding: 10px 12px;
757
+ font-size: 14px;
758
+ }
530
759
  }
531
760
 
532
761
  @keyframes pro6pp-spin {
@@ -535,21 +764,76 @@ var DEFAULT_STYLES = `
535
764
  `;
536
765
 
537
766
  // src/index.tsx
767
+ var HighlightedText = ({ text, query }) => {
768
+ const segments = useMemo(() => getHighlightSegments(text, query), [text, query]);
769
+ return /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__label" }, segments.map(
770
+ (seg, i) => seg.match ? /* @__PURE__ */ React.createElement("span", { key: i, className: "pro6pp-item__label--match" }, seg.text) : /* @__PURE__ */ React.createElement("span", { key: i, className: "pro6pp-item__label--unmatched" }, seg.text)
771
+ ));
772
+ };
538
773
  function useInfer(config) {
539
- const [state, setState] = useState(INITIAL_STATE);
774
+ const [state, setState] = useState(() => {
775
+ if (config.initialValue) {
776
+ const suffix = config.initialValue.addition ? ` ${config.initialValue.addition}` : "";
777
+ const postcodeStr = config.initialValue.postcode ? `${config.initialValue.postcode}, ` : "";
778
+ return {
779
+ ...INITIAL_STATE,
780
+ value: config.initialValue,
781
+ query: `${config.initialValue.street}, ${config.initialValue.street_number}${suffix}, ${postcodeStr}${config.initialValue.city}`,
782
+ isValid: true,
783
+ stage: "final"
784
+ };
785
+ }
786
+ return INITIAL_STATE;
787
+ });
788
+ const callbacksRef = useRef({
789
+ onStateChange: config.onStateChange,
790
+ onSelect: config.onSelect
791
+ });
792
+ useEffect(() => {
793
+ callbacksRef.current = {
794
+ onStateChange: config.onStateChange,
795
+ onSelect: config.onSelect
796
+ };
797
+ }, [config.onStateChange, config.onSelect]);
540
798
  const core = useMemo(() => {
541
- return new InferCore({
799
+ const instance = new InferCore({
542
800
  ...config,
543
801
  onStateChange: (newState) => {
544
802
  setState({ ...newState });
545
- if (config.onStateChange) {
546
- config.onStateChange(newState);
547
- }
803
+ callbacksRef.current.onStateChange?.(newState);
804
+ },
805
+ onSelect: (selection) => {
806
+ callbacksRef.current.onSelect?.(selection);
548
807
  }
549
808
  });
550
- }, [config.country, config.authKey, config.limit, config.debounceMs, config.maxRetries]);
809
+ if (config.initialValue) {
810
+ const address = config.initialValue;
811
+ const suffix = address.addition ? ` ${address.addition}` : "";
812
+ const postcodeStr = address.postcode ? `${address.postcode}, ` : "";
813
+ const label = `${address.street}, ${address.street_number}${suffix}, ${postcodeStr}${address.city}`;
814
+ instance.selectItem({ label, value: address });
815
+ }
816
+ return instance;
817
+ }, [
818
+ config.country,
819
+ config.authKey,
820
+ config.apiUrl,
821
+ config.fetcher,
822
+ config.limit,
823
+ config.debounceMs,
824
+ config.maxRetries,
825
+ config.initialValue,
826
+ config.language
827
+ ]);
828
+ const setValue = (address) => {
829
+ if (!address) return;
830
+ const suffix = address.addition ? ` ${address.addition}` : "";
831
+ const postcodeStr = address.postcode ? `${address.postcode}, ` : "";
832
+ const label = `${address.street}, ${address.street_number}${suffix}, ${postcodeStr}${address.city}`;
833
+ core.selectItem({ label, value: address });
834
+ };
551
835
  return {
552
- /** The current UI state (suggestions, loading status, query, etc.). */
836
+ /** The current UI state (suggestions, loading status, query, value, etc.). */
553
837
  state,
554
838
  /** The raw InferCore instance for manual control. */
555
839
  core,
@@ -559,160 +843,175 @@ function useInfer(config) {
559
843
  onChange: (e) => core.handleInput(e.target.value),
560
844
  onKeyDown: (e) => core.handleKeyDown(e)
561
845
  },
562
- /** Function to manually select a specific suggestion. */
846
+ /** Manually select a specific suggestion. */
563
847
  selectItem: (item) => core.selectItem(item),
564
- /** Function to load more results. */
848
+ /** Programmatically set the address value. */
849
+ setValue,
850
+ /** Load more results. */
565
851
  loadMore: () => core.loadMore()
566
852
  };
567
853
  }
568
- var Pro6PPInfer = ({
569
- className,
570
- style,
571
- inputProps,
572
- placeholder = "Start typing an address...",
573
- renderItem,
574
- disableDefaultStyles = false,
575
- noResultsText = "No results found",
576
- loadMoreText = "Show more results...",
577
- renderNoResults,
578
- showClearButton = true,
579
- ...config
580
- }) => {
581
- const { state, selectItem, loadMore, inputProps: coreInputProps, core } = useInfer(config);
582
- const [isOpen, setIsOpen] = useState(false);
583
- const inputRef = useRef(null);
584
- const wrapperRef = useRef(null);
585
- useEffect(() => {
586
- if (disableDefaultStyles) return;
587
- const styleId = "pro6pp-styles";
588
- if (!document.getElementById(styleId)) {
589
- const styleEl = document.createElement("style");
590
- styleEl.id = styleId;
591
- styleEl.textContent = DEFAULT_STYLES;
592
- document.head.appendChild(styleEl);
593
- }
594
- }, [disableDefaultStyles]);
595
- useEffect(() => {
596
- const handleClickOutside = (event) => {
597
- if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
854
+ var Pro6PPInfer = forwardRef(
855
+ ({
856
+ className,
857
+ style,
858
+ inputProps,
859
+ placeholder = "Start typing an address...",
860
+ renderItem,
861
+ disableDefaultStyles = false,
862
+ noResultsText = "No results found",
863
+ loadingText = "Loading more...",
864
+ renderNoResults,
865
+ showClearButton = true,
866
+ ...config
867
+ }, ref) => {
868
+ const { state, selectItem, loadMore, inputProps: coreInputProps, core } = useInfer(config);
869
+ const [isOpen, setIsOpen] = useState(false);
870
+ const internalInputRef = useRef(null);
871
+ const wrapperRef = useRef(null);
872
+ const observerTarget = useRef(null);
873
+ useImperativeHandle(ref, () => internalInputRef.current);
874
+ useEffect(() => {
875
+ if (disableDefaultStyles) return;
876
+ const styleId = "pro6pp-styles";
877
+ if (!document.getElementById(styleId)) {
878
+ const styleEl = document.createElement("style");
879
+ styleEl.id = styleId;
880
+ styleEl.textContent = DEFAULT_STYLES;
881
+ document.head.appendChild(styleEl);
882
+ }
883
+ }, [disableDefaultStyles]);
884
+ useEffect(() => {
885
+ const handleClickOutside = (event) => {
886
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
887
+ setIsOpen(false);
888
+ }
889
+ };
890
+ document.addEventListener("mousedown", handleClickOutside);
891
+ return () => document.removeEventListener("mousedown", handleClickOutside);
892
+ }, []);
893
+ useEffect(() => {
894
+ const currentTarget = observerTarget.current;
895
+ if (!currentTarget) return;
896
+ const observer = new IntersectionObserver(
897
+ (entries) => {
898
+ if (entries[0].isIntersecting && state.hasMore && !state.isLoading) {
899
+ loadMore();
900
+ }
901
+ },
902
+ { threshold: 0.1 }
903
+ );
904
+ observer.observe(currentTarget);
905
+ return () => {
906
+ if (currentTarget) observer.unobserve(currentTarget);
907
+ };
908
+ }, [state.hasMore, state.isLoading, loadMore, isOpen]);
909
+ const items = useMemo(() => {
910
+ return [
911
+ ...state.cities.map((c) => ({ ...c, type: "city" })),
912
+ ...state.streets.map((s) => ({ ...s, type: "street" })),
913
+ ...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
914
+ ];
915
+ }, [state.cities, state.streets, state.suggestions]);
916
+ const handleSelect = (item) => {
917
+ const isFinal = selectItem(item);
918
+ if (!isFinal) {
919
+ setTimeout(() => internalInputRef.current?.focus(), 0);
920
+ } else {
598
921
  setIsOpen(false);
599
922
  }
600
923
  };
601
- document.addEventListener("mousedown", handleClickOutside);
602
- return () => document.removeEventListener("mousedown", handleClickOutside);
603
- }, []);
604
- const items = useMemo(() => {
605
- return [
606
- ...state.cities.map((c) => ({ ...c, type: "city" })),
607
- ...state.streets.map((s) => ({ ...s, type: "street" })),
608
- ...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
609
- ];
610
- }, [state.cities, state.streets, state.suggestions]);
611
- const handleSelect = (item) => {
612
- selectItem(item);
613
- setIsOpen(false);
614
- if (!state.isValid && inputRef.current) {
615
- inputRef.current.focus();
616
- }
617
- };
618
- const handleClear = () => {
619
- core.handleInput("");
620
- if (inputRef.current) {
621
- inputRef.current.focus();
622
- }
623
- };
624
- const hasResults = items.length > 0;
625
- const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
626
- const showDropdown = isOpen && (hasResults || showNoResults);
627
- return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, className: `pro6pp-wrapper ${className || ""}`, style }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(
628
- "input",
629
- {
630
- ref: inputRef,
631
- type: "text",
632
- className: "pro6pp-input",
633
- placeholder,
634
- autoComplete: "off",
635
- ...inputProps,
636
- ...coreInputProps,
637
- onFocus: (e) => {
638
- setIsOpen(true);
639
- inputProps?.onFocus?.(e);
924
+ const handleClear = () => {
925
+ core.handleInput("");
926
+ internalInputRef.current?.focus();
927
+ };
928
+ const hasResults = items.length > 0;
929
+ const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
930
+ const showDropdown = isOpen && (hasResults || state.isLoading || showNoResults);
931
+ const isInfiniteLoading = state.isLoading && items.length > 0;
932
+ return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, className: `pro6pp-wrapper ${className || ""}`, style }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(
933
+ "input",
934
+ {
935
+ ref: internalInputRef,
936
+ type: "text",
937
+ className: "pro6pp-input",
938
+ placeholder,
939
+ autoComplete: "off",
940
+ autoCorrect: "off",
941
+ autoCapitalize: "none",
942
+ spellCheck: "false",
943
+ inputMode: "search",
944
+ enterKeyHint: "search",
945
+ ...inputProps,
946
+ ...coreInputProps,
947
+ onFocus: (e) => {
948
+ setIsOpen(true);
949
+ inputProps?.onFocus?.(e);
950
+ }
640
951
  }
641
- }
642
- ), /* @__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(
643
- "button",
644
- {
645
- type: "button",
646
- className: "pro6pp-clear-button",
647
- onClick: handleClear,
648
- "aria-label": "Clear input"
649
- },
650
- /* @__PURE__ */ React.createElement(
651
- "svg",
952
+ ), /* @__PURE__ */ React.createElement("div", { className: "pro6pp-input-addons" }, showClearButton && state.query.length > 0 && /* @__PURE__ */ React.createElement(
953
+ "button",
652
954
  {
653
- width: "14",
654
- height: "14",
655
- viewBox: "0 0 24 24",
656
- fill: "none",
657
- stroke: "currentColor",
658
- strokeWidth: "2",
659
- strokeLinecap: "round",
660
- strokeLinejoin: "round"
955
+ type: "button",
956
+ className: "pro6pp-clear-button",
957
+ onClick: handleClear,
958
+ "aria-label": "Clear input"
661
959
  },
662
- /* @__PURE__ */ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
663
- /* @__PURE__ */ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
664
- )
665
- ))), showDropdown && /* @__PURE__ */ React.createElement(
666
- "div",
667
- {
668
- className: "pro6pp-dropdown",
669
- onWheel: (e) => e.stopPropagation(),
670
- onMouseDown: (e) => e.stopPropagation()
671
- },
672
- /* @__PURE__ */ React.createElement("ul", { className: "pro6pp-list", role: "listbox" }, hasResults ? items.map((item, index) => {
673
- const isActive = index === state.selectedSuggestionIndex;
674
- const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
675
- const showChevron = item.value === void 0 || item.value === null;
676
- return /* @__PURE__ */ React.createElement(
677
- "li",
960
+ /* @__PURE__ */ React.createElement(
961
+ "svg",
678
962
  {
679
- key: `${item.label}-${index}`,
680
- role: "option",
681
- "aria-selected": isActive,
682
- className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
683
- onMouseDown: (e) => e.preventDefault(),
684
- onClick: () => handleSelect(item)
963
+ width: "14",
964
+ height: "14",
965
+ viewBox: "0 0 24 24",
966
+ fill: "none",
967
+ stroke: "currentColor",
968
+ strokeWidth: "2",
969
+ strokeLinecap: "round",
970
+ strokeLinejoin: "round"
685
971
  },
686
- renderItem ? renderItem(item, isActive) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__label" }, item.label), secondaryText && /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__subtitle" }, ", ", secondaryText), showChevron && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-item__chevron" }, /* @__PURE__ */ React.createElement(
687
- "svg",
688
- {
689
- width: "16",
690
- height: "16",
691
- viewBox: "0 0 24 24",
692
- fill: "none",
693
- stroke: "currentColor",
694
- strokeWidth: "2",
695
- strokeLinecap: "round",
696
- strokeLinejoin: "round"
697
- },
698
- /* @__PURE__ */ React.createElement("polyline", { points: "9 18 15 12 9 6" })
699
- )))
700
- );
701
- }) : /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)),
702
- state.hasMore && /* @__PURE__ */ React.createElement(
703
- "button",
972
+ /* @__PURE__ */ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
973
+ /* @__PURE__ */ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
974
+ )
975
+ ))), showDropdown && /* @__PURE__ */ React.createElement(
976
+ "div",
704
977
  {
705
- type: "button",
706
- className: "pro6pp-load-more",
707
- onClick: (e) => {
708
- e.preventDefault();
709
- loadMore();
710
- }
978
+ className: "pro6pp-dropdown",
979
+ onWheel: (e) => e.stopPropagation(),
980
+ onMouseDown: (e) => e.stopPropagation()
711
981
  },
712
- loadMoreText
713
- )
714
- ));
715
- };
982
+ /* @__PURE__ */ React.createElement("ul", { className: "pro6pp-list", role: "listbox" }, hasResults ? /* @__PURE__ */ React.createElement(React.Fragment, null, items.map((item, index) => {
983
+ const isActive = index === state.selectedSuggestionIndex;
984
+ const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
985
+ const showChevron = item.value === void 0 || item.value === null;
986
+ return /* @__PURE__ */ React.createElement(
987
+ "li",
988
+ {
989
+ key: `${item.label}-${index}`,
990
+ role: "option",
991
+ "aria-selected": isActive,
992
+ className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
993
+ onMouseDown: (e) => e.preventDefault(),
994
+ onClick: () => handleSelect(item)
995
+ },
996
+ 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(
997
+ "svg",
998
+ {
999
+ width: "16",
1000
+ height: "16",
1001
+ viewBox: "0 0 24 24",
1002
+ fill: "none",
1003
+ stroke: "currentColor",
1004
+ strokeWidth: "2",
1005
+ strokeLinecap: "round",
1006
+ strokeLinejoin: "round"
1007
+ },
1008
+ /* @__PURE__ */ React.createElement("polyline", { points: "9 18 15 12 9 6" })
1009
+ )))
1010
+ );
1011
+ }), state.hasMore && !state.isLoading && /* @__PURE__ */ React.createElement("li", { key: "sentinel", ref: observerTarget, style: { height: "1px", opacity: 0 } }), isInfiniteLoading && /* @__PURE__ */ React.createElement("li", { key: "loader", className: "pro6pp-loader-item" }, /* @__PURE__ */ React.createElement("div", { className: "pro6pp-mini-spinner" }), /* @__PURE__ */ React.createElement("span", null, loadingText))) : state.isLoading ? /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, "Searching...") : /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText))
1012
+ ));
1013
+ }
1014
+ );
716
1015
  export {
717
1016
  Pro6PPInfer,
718
1017
  useInfer