@pro6pp/infer-react 0.0.2-beta.8 → 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,15 +38,111 @@ __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",
44
- LIMIT: 1e3,
138
+ LIMIT: 20,
45
139
  DEBOUNCE_MS: 150,
46
- MIN_DEBOUNCE_MS: 50
140
+ MIN_DEBOUNCE_MS: 50,
141
+ MAX_RETRIES: 0
47
142
  };
48
143
  var PATTERNS = {
49
- DIGITS_1_3: /^[0-9]{1,3}$/
144
+ DIGITS_1_3: /^[0-9]{1,3}$/,
145
+ STREET_NUMBER_PREFIX: /^(\d+)\s*,\s*$/
50
146
  };
51
147
  var INITIAL_STATE = {
52
148
  query: "",
@@ -55,8 +151,10 @@ var INITIAL_STATE = {
55
151
  streets: [],
56
152
  suggestions: [],
57
153
  isValid: false,
154
+ value: null,
58
155
  isError: false,
59
156
  isLoading: false,
157
+ hasMore: false,
60
158
  selectedSuggestionIndex: -1
61
159
  };
62
160
  var InferCore = class {
@@ -67,8 +165,11 @@ var InferCore = class {
67
165
  constructor(config) {
68
166
  __publicField(this, "country");
69
167
  __publicField(this, "authKey");
70
- __publicField(this, "apiUrl");
71
- __publicField(this, "limit");
168
+ __publicField(this, "explicitApiUrl");
169
+ __publicField(this, "baseLimit");
170
+ __publicField(this, "currentLimit");
171
+ __publicField(this, "maxRetries");
172
+ __publicField(this, "language");
72
173
  __publicField(this, "fetcher");
73
174
  __publicField(this, "onStateChange");
74
175
  __publicField(this, "onSelect");
@@ -79,11 +180,14 @@ var InferCore = class {
79
180
  __publicField(this, "state");
80
181
  __publicField(this, "abortController", null);
81
182
  __publicField(this, "debouncedFetch");
82
- __publicField(this, "isSelecting", false);
83
183
  this.country = config.country;
84
184
  this.authKey = config.authKey;
85
- this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
86
- this.limit = config.limit || DEFAULTS.LIMIT;
185
+ this.explicitApiUrl = config.apiUrl;
186
+ this.baseLimit = config.limit || DEFAULTS.LIMIT;
187
+ this.currentLimit = this.baseLimit;
188
+ this.language = config.language;
189
+ const configRetries = config.maxRetries !== void 0 ? config.maxRetries : DEFAULTS.MAX_RETRIES;
190
+ this.maxRetries = Math.max(0, Math.min(configRetries, 10));
87
191
  this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
88
192
  this.onStateChange = config.onStateChange || (() => {
89
193
  });
@@ -100,28 +204,37 @@ var InferCore = class {
100
204
  * @param value The raw string from the input field.
101
205
  */
102
206
  handleInput(value) {
103
- if (this.isSelecting) {
104
- this.isSelecting = false;
105
- return;
106
- }
207
+ this.currentLimit = this.baseLimit;
107
208
  const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
108
209
  this.updateState({
109
210
  query: value,
110
211
  isValid: false,
212
+ value: null,
111
213
  isLoading: !!value.trim(),
112
- selectedSuggestionIndex: -1
214
+ selectedSuggestionIndex: -1,
215
+ hasMore: false,
216
+ stage: isEditingFinal ? null : this.state.stage
113
217
  });
114
218
  if (isEditingFinal) {
115
219
  this.onSelect(null);
116
220
  }
117
221
  this.debouncedFetch(value);
118
222
  }
223
+ /**
224
+ * Increases the current limit and re-fetches the query to show more results.
225
+ */
226
+ loadMore() {
227
+ if (this.state.isLoading) return;
228
+ this.currentLimit += this.baseLimit;
229
+ this.updateState({ isLoading: true });
230
+ this.executeFetch(this.state.query);
231
+ }
119
232
  /**
120
233
  * Handles keyboard events for the input field.
121
234
  * Supports:
122
235
  * - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
123
236
  * - `Enter`: Select the currently highlighted suggestion.
124
- * - `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.
125
238
  * @param event The keyboard event from the input element.
126
239
  */
127
240
  handleKeyDown(event) {
@@ -169,6 +282,7 @@ var InferCore = class {
169
282
  * Manually selects a suggestion or a string value.
170
283
  * This is typically called when a user clicks a suggestion in the UI.
171
284
  * @param item The suggestion object or string to select.
285
+ * @returns boolean True if the selection is a final address.
172
286
  */
173
287
  selectItem(item) {
174
288
  this.debouncedFetch.cancel();
@@ -182,26 +296,27 @@ var InferCore = class {
182
296
  }
183
297
  const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
184
298
  const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
185
- this.isSelecting = true;
186
299
  if (this.state.stage === "final" || isFullResult) {
187
300
  let finalQuery = label;
188
301
  if (valueObj && Object.keys(valueObj).length > 0) {
189
- const { street, street_number, house_number, city } = valueObj;
190
- const number = street_number || house_number;
191
- if (street && number && city) {
192
- 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}`;
193
307
  }
194
308
  }
195
309
  this.finishSelection(finalQuery, valueObj);
196
- return;
310
+ return true;
197
311
  }
198
312
  const subtitle = typeof item !== "string" ? item.subtitle : null;
199
313
  this.processSelection(logicValue, subtitle);
314
+ return false;
200
315
  }
201
316
  shouldAutoInsertComma(currentVal) {
202
317
  const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
203
318
  if (isStartOfSegmentAndNumeric) return true;
204
- if (this.state.stage === "house_number") {
319
+ if (this.state.stage === "street_number") {
205
320
  const currentFragment = this.getCurrentFragment(currentVal);
206
321
  return PATTERNS.DIGITS_1_3.test(currentFragment);
207
322
  }
@@ -214,12 +329,11 @@ var InferCore = class {
214
329
  cities: [],
215
330
  streets: [],
216
331
  isValid: true,
217
- stage: "final"
332
+ value: value || null,
333
+ stage: "final",
334
+ hasMore: false
218
335
  });
219
336
  this.onSelect(value || label);
220
- setTimeout(() => {
221
- this.isSelecting = false;
222
- }, 0);
223
337
  }
224
338
  processSelection(text, subtitle) {
225
339
  const { stage, query } = this.state;
@@ -230,7 +344,22 @@ var InferCore = class {
230
344
  nextQuery = `${subtitle}, ${text}, `;
231
345
  } else {
232
346
  const prefix = this.getQueryPrefix(query);
233
- 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
+ }
234
363
  }
235
364
  this.updateQueryAndFetch(nextQuery);
236
365
  return;
@@ -240,50 +369,78 @@ var InferCore = class {
240
369
  return;
241
370
  }
242
371
  const hasComma = query.includes(",");
243
- const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
372
+ const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "street_number_first");
244
373
  if (isFirstSegment) {
245
374
  nextQuery = `${text}, `;
246
375
  } else {
247
376
  nextQuery = this.replaceLastSegment(query, text);
248
- if (stage !== "house_number") {
377
+ if (stage !== "street_number") {
249
378
  nextQuery += ", ";
250
379
  }
251
380
  }
252
381
  this.updateQueryAndFetch(nextQuery);
253
382
  }
254
- executeFetch(val) {
383
+ executeFetch(val, attempt = 0) {
255
384
  const text = (val || "").toString();
256
385
  if (!text.trim()) {
257
386
  this.abortController?.abort();
258
387
  this.resetState();
259
388
  return;
260
389
  }
261
- this.updateState({ isError: false });
262
- if (this.abortController) this.abortController.abort();
263
- this.abortController = new AbortController();
264
- const url = new URL(`${this.apiUrl}/infer/${this.country.toLowerCase()}`);
265
- const params = {
266
- authKey: this.authKey,
390
+ if (attempt === 0) {
391
+ this.updateState({ isError: false });
392
+ if (this.abortController) this.abortController.abort();
393
+ this.abortController = new AbortController();
394
+ }
395
+ const currentSignal = this.abortController?.signal;
396
+ const baseUrl = this.explicitApiUrl ? this.explicitApiUrl : `${DEFAULTS.API_URL}/infer/${this.country.toLowerCase()}`;
397
+ const params = new URLSearchParams({
267
398
  query: text,
268
- limit: this.limit.toString()
269
- };
270
- url.search = new URLSearchParams(params).toString();
271
- this.fetcher(url.toString(), { signal: this.abortController.signal }).then((res) => {
272
- if (!res.ok) throw new Error("Network error");
399
+ limit: this.currentLimit.toString()
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) => {
413
+ if (!res.ok) {
414
+ if (attempt < this.maxRetries && (res.status >= 500 || res.status === 429)) {
415
+ return this.retry(val, attempt, currentSignal);
416
+ }
417
+ throw new Error("Network error");
418
+ }
273
419
  return res.json();
274
- }).then((data) => this.mapResponseToState(data)).catch((e) => {
275
- if (e.name !== "AbortError") {
276
- this.updateState({ isError: true, isLoading: false });
420
+ }).then((data) => {
421
+ if (data) this.mapResponseToState(data);
422
+ }).catch((e) => {
423
+ if (e.name === "AbortError") return;
424
+ if (attempt < this.maxRetries) {
425
+ return this.retry(val, attempt, currentSignal);
277
426
  }
427
+ this.updateState({ isError: true, isLoading: false });
278
428
  });
279
429
  }
430
+ retry(val, attempt, signal) {
431
+ if (signal?.aborted) return;
432
+ const delay = Math.pow(2, attempt) * 200;
433
+ setTimeout(() => {
434
+ if (!signal?.aborted) {
435
+ this.executeFetch(val, attempt + 1);
436
+ }
437
+ }, delay);
438
+ }
280
439
  mapResponseToState(data) {
281
440
  const newState = {
282
441
  stage: data.stage,
283
442
  isLoading: false
284
443
  };
285
- let autoSelect = false;
286
- let autoSelectItem = null;
287
444
  const rawSuggestions = data.suggestions || [];
288
445
  const uniqueSuggestions = [];
289
446
  const seen = /* @__PURE__ */ new Set();
@@ -291,43 +448,62 @@ var InferCore = class {
291
448
  const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
292
449
  if (!seen.has(key)) {
293
450
  seen.add(key);
294
- uniqueSuggestions.push(item);
451
+ const reformattedItem = this.reformatSuggestionLabel(item);
452
+ uniqueSuggestions.push(reformattedItem);
295
453
  }
296
454
  }
455
+ const totalCount = uniqueSuggestions.length + (data.cities?.length || 0) + (data.streets?.length || 0);
456
+ newState.hasMore = totalCount >= this.currentLimit;
297
457
  if (data.stage === "mixed") {
298
458
  newState.cities = data.cities || [];
299
459
  newState.streets = data.streets || [];
300
- newState.suggestions = [];
460
+ if (newState.cities?.length === 0 && newState.streets?.length === 0) {
461
+ newState.suggestions = uniqueSuggestions;
462
+ } else {
463
+ newState.suggestions = [];
464
+ }
301
465
  } else {
302
466
  newState.suggestions = uniqueSuggestions;
303
467
  newState.cities = [];
304
468
  newState.streets = [];
305
- if (data.stage === "final" && uniqueSuggestions.length === 1) {
306
- autoSelect = true;
307
- autoSelectItem = uniqueSuggestions[0];
308
- }
309
469
  }
310
470
  newState.isValid = data.stage === "final";
311
- if (autoSelect && autoSelectItem) {
312
- newState.query = autoSelectItem.label;
313
- newState.suggestions = [];
314
- newState.cities = [];
315
- newState.streets = [];
316
- newState.isValid = true;
317
- this.updateState(newState);
318
- const val = typeof autoSelectItem.value === "object" ? autoSelectItem.value : autoSelectItem.label;
319
- this.onSelect(val);
320
- } else {
321
- this.updateState(newState);
471
+ this.updateState(newState);
472
+ if (newState.isValid && uniqueSuggestions.length === 1) {
473
+ this.selectItem(uniqueSuggestions[0]);
322
474
  }
323
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;
488
+ }
489
+ const reformattedLabel = formatLabelByInputOrder(this.state.query, addressValue);
490
+ if (reformattedLabel) {
491
+ return { ...item, label: reformattedLabel };
492
+ }
493
+ return item;
494
+ }
324
495
  updateQueryAndFetch(nextQuery) {
325
- this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
326
- this.updateState({ isLoading: true, isValid: 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
+ });
327
506
  this.debouncedFetch(nextQuery);
328
- setTimeout(() => {
329
- this.isSelecting = false;
330
- }, 0);
331
507
  }
332
508
  replaceLastSegment(fullText, newSegment) {
333
509
  const lastCommaIndex = fullText.lastIndexOf(",");
@@ -364,6 +540,49 @@ var InferCore = class {
364
540
  }
365
541
  };
366
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
+
367
586
  // ../core/src/default-styles.ts
368
587
  var DEFAULT_STYLES = `
369
588
  .pro6pp-wrapper {
@@ -371,116 +590,276 @@ var DEFAULT_STYLES = `
371
590
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
372
591
  box-sizing: border-box;
373
592
  width: 100%;
593
+ -webkit-tap-highlight-color: transparent;
374
594
  }
375
595
  .pro6pp-wrapper * {
376
596
  box-sizing: border-box;
377
597
  }
378
598
  .pro6pp-input {
379
599
  width: 100%;
380
- padding: 10px 12px;
600
+ padding: 12px 14px;
601
+ padding-right: 48px;
381
602
  border: 1px solid #e0e0e0;
382
- border-radius: 4px;
603
+ border-radius: 8px;
383
604
  font-size: 16px;
384
605
  line-height: 1.5;
606
+ appearance: none;
385
607
  transition: border-color 0.2s, box-shadow 0.2s;
386
608
  }
609
+
610
+ .pro6pp-input::placeholder {
611
+ font-size: 16px;
612
+ color: #a3a3a3;
613
+ }
614
+
387
615
  .pro6pp-input:focus {
388
616
  outline: none;
389
617
  border-color: #3b82f6;
390
618
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
391
619
  }
620
+
621
+ .pro6pp-input-addons {
622
+ position: absolute;
623
+ right: 4px;
624
+ top: 0;
625
+ bottom: 0;
626
+ display: flex;
627
+ align-items: center;
628
+ pointer-events: none;
629
+ }
630
+ .pro6pp-input-addons > * {
631
+ pointer-events: auto;
632
+ }
633
+
634
+ .pro6pp-clear-button {
635
+ background: none;
636
+ border: none;
637
+ width: 32px;
638
+ height: 32px;
639
+ cursor: pointer;
640
+ color: #a3a3a3;
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: center;
644
+ border-radius: 50%;
645
+ transition: color 0.2s, background-color 0.2s;
646
+ touch-action: manipulation;
647
+ }
648
+
649
+ @media (hover: hover) {
650
+ .pro6pp-clear-button:hover {
651
+ color: #1f2937;
652
+ background-color: #f3f4f6;
653
+ }
654
+ }
655
+
656
+ .pro6pp-clear-button:active {
657
+ background-color: #f3f4f6;
658
+ }
659
+
392
660
  .pro6pp-dropdown {
393
661
  position: absolute;
394
662
  top: 100%;
395
663
  left: 0;
396
664
  right: 0;
397
- z-index: 9999;
398
665
  margin-top: 4px;
399
- background: white;
400
- border: 1px solid #e0e0e0;
401
- border-radius: 4px;
402
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
403
- 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;
404
673
  overflow-y: auto;
405
- list-style: none !important;
406
- padding: 0 !important;
407
- margin: 0 !important;
674
+ display: flex;
675
+ flex-direction: column;
676
+ }
677
+
678
+ @media (max-height: 500px) {
679
+ .pro6pp-dropdown {
680
+ max-height: 180px;
681
+ }
408
682
  }
683
+
684
+ .pro6pp-list {
685
+ list-style: none;
686
+ margin: 0;
687
+ padding: 0;
688
+ width: 100%;
689
+ }
690
+
409
691
  .pro6pp-item {
410
- padding: 10px 16px;
692
+ padding: 12px 14px;
411
693
  cursor: pointer;
412
694
  display: flex;
413
- flex-direction: row;
414
695
  align-items: center;
415
- color: #000000;
416
- font-size: 14px;
417
- line-height: 1.2;
418
- white-space: nowrap;
419
- 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;
420
702
  }
421
- .pro6pp-item:hover, .pro6pp-item--active {
422
- background-color: #f5f5f5;
703
+
704
+ .pro6pp-item:last-child {
705
+ border-bottom: none;
423
706
  }
424
- .pro6pp-item__label {
425
- font-weight: 500;
426
- flex-shrink: 0;
707
+
708
+ @media (hover: hover) {
709
+ .pro6pp-item:hover, .pro6pp-item--active {
710
+ background-color: #f9fafb;
711
+ }
427
712
  }
428
- .pro6pp-item__subtitle {
429
- font-size: 14px;
430
- color: #404040;
713
+
714
+ .pro6pp-item:active {
715
+ background-color: #f3f4f6;
716
+ }
717
+
718
+ .pro6pp-item__label {
719
+ font-weight: 400;
720
+ flex-shrink: 1;
431
721
  overflow: hidden;
432
722
  text-overflow: ellipsis;
433
- flex-shrink: 1;
723
+ white-space: nowrap;
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
+
735
+ .pro6pp-item__subtitle {
736
+ color: #6b7280;
737
+ flex-shrink: 0;
434
738
  }
739
+
435
740
  .pro6pp-item__chevron {
436
- margin-left: auto;
741
+ color: #d1d5db;
437
742
  display: flex;
438
743
  align-items: center;
439
- color: #a3a3a3;
744
+ margin-left: auto;
440
745
  padding-left: 8px;
441
746
  }
747
+
442
748
  .pro6pp-no-results {
443
- padding: 12px;
444
- color: #555555;
445
- font-size: 14px;
749
+ padding: 24px 16px;
750
+ color: #6b7280;
751
+ font-size: 15px;
446
752
  text-align: center;
447
- user-select: none;
448
- pointer-events: none;
449
753
  }
450
- .pro6pp-loader {
451
- position: absolute;
452
- right: 12px;
453
- top: 50%;
454
- transform: translateY(-50%);
455
- width: 16px;
456
- height: 16px;
457
- border: 2px solid #e0e0e0;
458
- border-top-color: #404040;
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;
765
+ }
766
+
767
+ .pro6pp-mini-spinner {
768
+ width: 14px;
769
+ height: 14px;
770
+ border: 2px solid #e5e7eb;
771
+ border-top-color: #6b7280;
459
772
  border-radius: 50%;
460
773
  animation: pro6pp-spin 0.6s linear infinite;
461
- pointer-events: none;
462
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
+ }
785
+ }
786
+
463
787
  @keyframes pro6pp-spin {
464
- to { transform: translateY(-50%) rotate(360deg); }
788
+ to { transform: rotate(360deg); }
465
789
  }
466
790
  `;
467
791
 
468
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
+ };
469
799
  function useInfer(config) {
470
- 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]);
471
824
  const core = (0, import_react.useMemo)(() => {
472
- return new InferCore({
825
+ const instance = new InferCore({
473
826
  ...config,
474
827
  onStateChange: (newState) => {
475
828
  setState({ ...newState });
476
- if (config.onStateChange) {
477
- config.onStateChange(newState);
478
- }
829
+ callbacksRef.current.onStateChange?.(newState);
830
+ },
831
+ onSelect: (selection) => {
832
+ callbacksRef.current.onSelect?.(selection);
479
833
  }
480
834
  });
481
- }, [config.country, config.authKey, config.limit, config.debounceMs]);
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
+ };
482
861
  return {
483
- /** The current UI state (suggestions, loading status, query, etc.). */
862
+ /** The current UI state (suggestions, loading status, query, value, etc.). */
484
863
  state,
485
864
  /** The raw InferCore instance for manual control. */
486
865
  core,
@@ -490,95 +869,125 @@ function useInfer(config) {
490
869
  onChange: (e) => core.handleInput(e.target.value),
491
870
  onKeyDown: (e) => core.handleKeyDown(e)
492
871
  },
493
- /** Function to manually select a specific suggestion. */
494
- selectItem: (item) => core.selectItem(item)
872
+ /** Manually select a specific suggestion. */
873
+ selectItem: (item) => core.selectItem(item),
874
+ /** Programmatically set the address value. */
875
+ setValue,
876
+ /** Load more results. */
877
+ loadMore: () => core.loadMore()
495
878
  };
496
879
  }
497
- var Pro6PPInfer = ({
498
- className,
499
- style,
500
- inputProps,
501
- placeholder = "Start typing an address...",
502
- renderItem,
503
- disableDefaultStyles = false,
504
- noResultsText = "No results found",
505
- renderNoResults,
506
- ...config
507
- }) => {
508
- const { state, selectItem, inputProps: coreInputProps } = useInfer(config);
509
- const [isOpen, setIsOpen] = (0, import_react.useState)(false);
510
- const inputRef = (0, import_react.useRef)(null);
511
- const wrapperRef = (0, import_react.useRef)(null);
512
- (0, import_react.useEffect)(() => {
513
- if (disableDefaultStyles) return;
514
- const styleId = "pro6pp-styles";
515
- if (!document.getElementById(styleId)) {
516
- const styleEl = document.createElement("style");
517
- styleEl.id = styleId;
518
- styleEl.textContent = DEFAULT_STYLES;
519
- document.head.appendChild(styleEl);
520
- }
521
- }, [disableDefaultStyles]);
522
- (0, import_react.useEffect)(() => {
523
- const handleClickOutside = (event) => {
524
- 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 {
525
947
  setIsOpen(false);
526
948
  }
527
949
  };
528
- document.addEventListener("mousedown", handleClickOutside);
529
- return () => document.removeEventListener("mousedown", handleClickOutside);
530
- }, []);
531
- const items = (0, import_react.useMemo)(() => {
532
- return [
533
- ...state.cities.map((c) => ({ ...c, type: "city" })),
534
- ...state.streets.map((s) => ({ ...s, type: "street" })),
535
- ...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
536
- ];
537
- }, [state.cities, state.streets, state.suggestions]);
538
- const handleSelect = (item) => {
539
- selectItem(item);
540
- setIsOpen(false);
541
- if (!state.isValid && inputRef.current) {
542
- inputRef.current.focus();
543
- }
544
- };
545
- const hasResults = items.length > 0;
546
- const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
547
- const showDropdown = isOpen && (hasResults || showNoResults);
548
- 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(
549
- "input",
550
- {
551
- ref: inputRef,
552
- type: "text",
553
- className: "pro6pp-input",
554
- placeholder,
555
- autoComplete: "off",
556
- ...inputProps,
557
- ...coreInputProps,
558
- onFocus: (e) => {
559
- setIsOpen(true);
560
- 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
+ }
561
977
  }
562
- }
563
- ), state.isLoading && /* @__PURE__ */ import_react.default.createElement("div", { className: "pro6pp-loader" })), showDropdown && /* @__PURE__ */ import_react.default.createElement("ul", { className: "pro6pp-dropdown", role: "listbox" }, hasResults ? items.map((item, index) => {
564
- const isActive = index === state.selectedSuggestionIndex;
565
- const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
566
- const showChevron = item.value === void 0 || item.value === null;
567
- return /* @__PURE__ */ import_react.default.createElement(
568
- "li",
978
+ ), /* @__PURE__ */ import_react.default.createElement("div", { className: "pro6pp-input-addons" }, showClearButton && state.query.length > 0 && /* @__PURE__ */ import_react.default.createElement(
979
+ "button",
569
980
  {
570
- key: `${item.label}-${index}`,
571
- role: "option",
572
- "aria-selected": isActive,
573
- className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
574
- onMouseDown: (e) => e.preventDefault(),
575
- onClick: () => handleSelect(item)
981
+ type: "button",
982
+ className: "pro6pp-clear-button",
983
+ onClick: handleClear,
984
+ "aria-label": "Clear input"
576
985
  },
577
- 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(
986
+ /* @__PURE__ */ import_react.default.createElement(
578
987
  "svg",
579
988
  {
580
- width: "16",
581
- height: "16",
989
+ width: "14",
990
+ height: "14",
582
991
  viewBox: "0 0 24 24",
583
992
  fill: "none",
584
993
  stroke: "currentColor",
@@ -586,11 +995,49 @@ var Pro6PPInfer = ({
586
995
  strokeLinecap: "round",
587
996
  strokeLinejoin: "round"
588
997
  },
589
- /* @__PURE__ */ import_react.default.createElement("polyline", { points: "9 18 15 12 9 6" })
590
- )))
591
- );
592
- }) : /* @__PURE__ */ import_react.default.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)));
593
- };
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",
1003
+ {
1004
+ className: "pro6pp-dropdown",
1005
+ onWheel: (e) => e.stopPropagation(),
1006
+ onMouseDown: (e) => e.stopPropagation()
1007
+ },
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
+ );
594
1041
  // Annotate the CommonJS export names for ESM import in node:
595
1042
  0 && (module.exports = {
596
1043
  Pro6PPInfer,