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