@leavepulse/ui 0.16.2 → 0.17.0

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.
@@ -0,0 +1,366 @@
1
+ import { defineComponent, defineAsyncComponent, computed, ref, onMounted, watch, openBlock, createElementBlock, createVNode, withCtx, createElementVNode, unref, createBlock, createCommentVNode, createTextVNode, toDisplayString, renderSlot, normalizeClass } from "vue";
2
+ import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress } from "../components/address.js";
3
+ import { flagEmoji, loadCountries } from "../components/countries.js";
4
+ import { _ as _sfc_main$2 } from "./LpAutocomplete-DIgXmtCA.js";
5
+ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
6
+ import { _ as _sfc_main$1 } from "./LpFormField-Bn-ZwC6z.js";
7
+ import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
8
+ import { _ as _sfc_main$5 } from "./LpInput-BauZRyzm.js";
9
+ import { _ as _sfc_main$7 } from "./LpModal-CezA8eJ3.js";
10
+ import { _ as _sfc_main$6 } from "./LpSelect-D5mU4cZO.js";
11
+ const _hoisted_1 = { class: "flex flex-col gap-3" };
12
+ const _hoisted_2 = { class: "flex items-stretch gap-2" };
13
+ const _hoisted_3 = { class: "grid gap-3 sm:grid-cols-[minmax(0,1fr)_10rem]" };
14
+ const _hoisted_4 = {
15
+ key: 1,
16
+ class: "flex items-center gap-2"
17
+ };
18
+ const _hoisted_5 = {
19
+ key: 0,
20
+ class: "text-xs text-muted"
21
+ };
22
+ const _sfc_main = /* @__PURE__ */ defineComponent({
23
+ __name: "LpAddressInput",
24
+ props: {
25
+ modelValue: {},
26
+ provider: {},
27
+ tiles: { default: () => OSM_TILES },
28
+ only: {},
29
+ secondLine: { type: Boolean },
30
+ minChars: { default: 3 },
31
+ debounce: { default: 350 },
32
+ language: {},
33
+ disabled: { type: Boolean },
34
+ size: { default: "md" },
35
+ hideMap: { type: Boolean },
36
+ labels: {}
37
+ },
38
+ emits: ["update:modelValue", "pick"],
39
+ setup(__props, { emit: __emit }) {
40
+ const LpMapPicker = defineAsyncComponent(() => import("../components/LpMapPicker.vue.js"));
41
+ const props = __props;
42
+ const emit = __emit;
43
+ const DEFAULT_LABELS = {
44
+ line: "Street and number",
45
+ linePlaceholder: "Baker Street 221B",
46
+ secondLine: "Apartment, suite (optional)",
47
+ secondLinePlaceholder: "Flat 4",
48
+ city: "City",
49
+ cityPlaceholder: "London",
50
+ postalCode: "Postcode",
51
+ postalCodePlaceholder: "NW1 6XE",
52
+ country: "Country",
53
+ countryPlaceholder: "Select country…",
54
+ countrySearch: "Search country…",
55
+ pickOnMap: "Pick on map",
56
+ mapTitle: "Pick a location",
57
+ mapHint: "Click the map to drop a pin, drag to move around.",
58
+ use: "Use this address",
59
+ cancel: "Cancel",
60
+ searching: "Searching…",
61
+ noSuggestions: "No suggestions",
62
+ nothingHere: "No address found here"
63
+ };
64
+ const text = computed(() => ({ ...DEFAULT_LABELS, ...props.labels }));
65
+ const fallbackProvider = createNominatimProvider();
66
+ const geocoder = computed(() => props.provider ?? fallbackProvider);
67
+ const value = computed(() => props.modelValue ?? emptyAddress());
68
+ function patch(changes) {
69
+ emit("update:modelValue", { ...value.value, ...changes });
70
+ }
71
+ const countries = ref([]);
72
+ const countryOptions = computed(
73
+ () => countries.value.map((country) => ({
74
+ value: country.iso2,
75
+ label: `${flagEmoji(country.iso2)} ${country.name}`
76
+ }))
77
+ );
78
+ onMounted(async () => {
79
+ const list = await loadCountries();
80
+ const only = props.only?.map((code) => code.toUpperCase());
81
+ countries.value = only?.length ? list.filter((c) => only.includes(c.iso2)) : list;
82
+ });
83
+ function useSuggestions(field) {
84
+ const options = ref([]);
85
+ const loading = ref(false);
86
+ let timer;
87
+ let controller;
88
+ function query(term) {
89
+ if (timer) clearTimeout(timer);
90
+ controller?.abort();
91
+ if (term.trim().length < props.minChars) {
92
+ options.value = [];
93
+ loading.value = false;
94
+ return;
95
+ }
96
+ loading.value = true;
97
+ timer = setTimeout(async () => {
98
+ controller = new AbortController();
99
+ try {
100
+ options.value = await geocoder.value.search(term.trim(), field, {
101
+ country: value.value.country || void 0,
102
+ language: props.language,
103
+ signal: controller.signal
104
+ });
105
+ } catch {
106
+ options.value = [];
107
+ } finally {
108
+ loading.value = false;
109
+ }
110
+ }, props.debounce);
111
+ }
112
+ return { options, loading, query };
113
+ }
114
+ const lineSuggestions = useSuggestions("line");
115
+ const citySuggestions = useSuggestions("city");
116
+ function applySuggestion(suggestion, keep) {
117
+ const parsed = suggestion.address;
118
+ const changes = { lat: suggestion.lat, lon: suggestion.lon };
119
+ if (parsed.line && keep !== "line") changes.line = parsed.line;
120
+ if (parsed.city) changes.city = parsed.city;
121
+ if (parsed.postalCode) changes.postalCode = parsed.postalCode;
122
+ if (parsed.country) changes.country = parsed.country;
123
+ patch(changes);
124
+ }
125
+ function onLineSelect(chosen) {
126
+ const suggestion = lineSuggestions.options.value.find((option) => option.value === chosen);
127
+ if (suggestion) applySuggestion(suggestion, "line");
128
+ }
129
+ function onCitySelect(chosen) {
130
+ const suggestion = citySuggestions.options.value.find((option) => option.value === chosen);
131
+ if (suggestion) applySuggestion(suggestion, "city");
132
+ }
133
+ const mapOpen = ref(false);
134
+ const point = ref();
135
+ const resolved = ref();
136
+ const resolving = ref(false);
137
+ computed(
138
+ () => countries.value.find((c) => c.iso2 === value.value.country)?.name
139
+ );
140
+ const preview = computed(() => {
141
+ if (resolving.value) return text.value.searching;
142
+ if (!point.value) return text.value.mapHint;
143
+ if (!resolved.value) return text.value.nothingHere;
144
+ const parsed = resolved.value.address;
145
+ return formatAddress(
146
+ {
147
+ line: parsed.line ?? "",
148
+ city: parsed.city ?? "",
149
+ postalCode: parsed.postalCode ?? "",
150
+ country: parsed.country ?? ""
151
+ },
152
+ countries.value.find((c) => c.iso2 === parsed.country)?.name
153
+ );
154
+ });
155
+ watch(mapOpen, (open) => {
156
+ if (!open) return;
157
+ point.value = value.value.lat != null && value.value.lon != null ? { lat: value.value.lat, lon: value.value.lon } : void 0;
158
+ resolved.value = void 0;
159
+ });
160
+ async function onPick(next) {
161
+ point.value = next;
162
+ resolving.value = true;
163
+ resolved.value = void 0;
164
+ try {
165
+ resolved.value = await geocoder.value.reverse(next.lat, next.lon, {
166
+ language: props.language
167
+ });
168
+ } catch {
169
+ resolved.value = void 0;
170
+ } finally {
171
+ resolving.value = false;
172
+ }
173
+ }
174
+ function confirmPoint() {
175
+ if (!point.value) return;
176
+ const parsed = resolved.value?.address ?? {};
177
+ patch({
178
+ line: parsed.line ?? value.value.line,
179
+ city: parsed.city ?? value.value.city,
180
+ postalCode: parsed.postalCode ?? value.value.postalCode,
181
+ country: parsed.country ?? value.value.country,
182
+ lat: point.value.lat,
183
+ lon: point.value.lon
184
+ });
185
+ emit("pick", point.value);
186
+ mapOpen.value = false;
187
+ }
188
+ return (_ctx, _cache) => {
189
+ return openBlock(), createElementBlock("div", _hoisted_1, [
190
+ createVNode(_sfc_main$1, {
191
+ label: text.value.line
192
+ }, {
193
+ default: withCtx(() => [
194
+ createElementVNode("div", _hoisted_2, [
195
+ createVNode(_sfc_main$2, {
196
+ class: "min-w-0 flex-1",
197
+ "model-value": value.value.line,
198
+ options: unref(lineSuggestions).options.value,
199
+ loading: unref(lineSuggestions).loading.value,
200
+ filter: false,
201
+ "min-chars": __props.minChars,
202
+ disabled: __props.disabled,
203
+ size: __props.size,
204
+ placeholder: text.value.linePlaceholder,
205
+ "empty-text": text.value.noSuggestions,
206
+ "loading-text": text.value.searching,
207
+ icon: "lucide:map-pin",
208
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = (next) => {
209
+ patch({ line: next });
210
+ unref(lineSuggestions).query(next);
211
+ }),
212
+ onSelect: onLineSelect
213
+ }, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "size", "placeholder", "empty-text", "loading-text"]),
214
+ !__props.hideMap ? (openBlock(), createBlock(_sfc_main$3, {
215
+ key: 0,
216
+ variant: "soft",
217
+ size: __props.size,
218
+ square: "",
219
+ disabled: __props.disabled,
220
+ title: text.value.pickOnMap,
221
+ "aria-label": text.value.pickOnMap,
222
+ onClick: _cache[1] || (_cache[1] = ($event) => mapOpen.value = true)
223
+ }, {
224
+ default: withCtx(() => [
225
+ createVNode(_sfc_main$4, {
226
+ name: "lucide:map",
227
+ size: 16
228
+ })
229
+ ]),
230
+ _: 1
231
+ }, 8, ["size", "disabled", "title", "aria-label"])) : createCommentVNode("", true)
232
+ ])
233
+ ]),
234
+ _: 1
235
+ }, 8, ["label"]),
236
+ __props.secondLine ? (openBlock(), createBlock(_sfc_main$5, {
237
+ key: 0,
238
+ "model-value": value.value.line2 ?? "",
239
+ label: text.value.secondLine,
240
+ placeholder: text.value.secondLinePlaceholder,
241
+ disabled: __props.disabled,
242
+ size: __props.size,
243
+ autocomplete: "address-line2",
244
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = (next) => patch({ line2: next }))
245
+ }, null, 8, ["model-value", "label", "placeholder", "disabled", "size"])) : createCommentVNode("", true),
246
+ createElementVNode("div", _hoisted_3, [
247
+ createVNode(_sfc_main$1, {
248
+ label: text.value.city
249
+ }, {
250
+ default: withCtx(() => [
251
+ createVNode(_sfc_main$2, {
252
+ "model-value": value.value.city,
253
+ options: unref(citySuggestions).options.value,
254
+ loading: unref(citySuggestions).loading.value,
255
+ filter: false,
256
+ "min-chars": __props.minChars,
257
+ disabled: __props.disabled,
258
+ size: __props.size,
259
+ placeholder: text.value.cityPlaceholder,
260
+ "empty-text": text.value.noSuggestions,
261
+ "loading-text": text.value.searching,
262
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = (next) => {
263
+ patch({ city: next });
264
+ unref(citySuggestions).query(next);
265
+ }),
266
+ onSelect: onCitySelect
267
+ }, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "size", "placeholder", "empty-text", "loading-text"])
268
+ ]),
269
+ _: 1
270
+ }, 8, ["label"]),
271
+ createVNode(_sfc_main$5, {
272
+ "model-value": value.value.postalCode,
273
+ label: text.value.postalCode,
274
+ placeholder: text.value.postalCodePlaceholder,
275
+ disabled: __props.disabled,
276
+ size: __props.size,
277
+ autocomplete: "postal-code",
278
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = (next) => patch({ postalCode: next }))
279
+ }, null, 8, ["model-value", "label", "placeholder", "disabled", "size"])
280
+ ]),
281
+ createVNode(_sfc_main$1, {
282
+ label: text.value.country
283
+ }, {
284
+ default: withCtx(() => [
285
+ createVNode(_sfc_main$6, {
286
+ "model-value": value.value.country,
287
+ options: countryOptions.value,
288
+ placeholder: text.value.countryPlaceholder,
289
+ "search-placeholder": text.value.countrySearch,
290
+ disabled: __props.disabled,
291
+ size: __props.size,
292
+ searchable: "",
293
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = (next) => patch({ country: String(next ?? "") }))
294
+ }, null, 8, ["model-value", "options", "placeholder", "search-placeholder", "disabled", "size"])
295
+ ]),
296
+ _: 1
297
+ }, 8, ["label"]),
298
+ !__props.hideMap ? (openBlock(), createElementBlock("div", _hoisted_4, [
299
+ createVNode(_sfc_main$3, {
300
+ variant: "soft",
301
+ size: __props.size === "lg" ? "md" : "sm",
302
+ disabled: __props.disabled,
303
+ onClick: _cache[6] || (_cache[6] = ($event) => mapOpen.value = true)
304
+ }, {
305
+ default: withCtx(() => [
306
+ createVNode(_sfc_main$4, {
307
+ name: "lucide:map",
308
+ size: 15
309
+ }),
310
+ createTextVNode(" " + toDisplayString(text.value.pickOnMap), 1)
311
+ ]),
312
+ _: 1
313
+ }, 8, ["size", "disabled"]),
314
+ value.value.lat != null && value.value.lon != null ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(value.value.lat.toFixed(5)) + ", " + toDisplayString(value.value.lon.toFixed(5)), 1)) : createCommentVNode("", true)
315
+ ])) : createCommentVNode("", true),
316
+ createVNode(_sfc_main$7, {
317
+ open: mapOpen.value,
318
+ "onUpdate:open": _cache[8] || (_cache[8] = ($event) => mapOpen.value = $event),
319
+ title: text.value.mapTitle,
320
+ size: "2xl"
321
+ }, {
322
+ footer: withCtx(() => [
323
+ createVNode(_sfc_main$3, {
324
+ variant: "ghost",
325
+ onClick: _cache[7] || (_cache[7] = ($event) => mapOpen.value = false)
326
+ }, {
327
+ default: withCtx(() => [
328
+ createTextVNode(toDisplayString(text.value.cancel), 1)
329
+ ]),
330
+ _: 1
331
+ }),
332
+ createVNode(_sfc_main$3, {
333
+ disabled: !point.value,
334
+ onClick: confirmPoint
335
+ }, {
336
+ default: withCtx(() => [
337
+ createTextVNode(toDisplayString(text.value.use), 1)
338
+ ]),
339
+ _: 1
340
+ }, 8, ["disabled"])
341
+ ]),
342
+ default: withCtx(() => [
343
+ renderSlot(_ctx.$slots, "map", {
344
+ point: point.value,
345
+ setPoint: onPick,
346
+ tiles: __props.tiles
347
+ }, () => [
348
+ createVNode(unref(LpMapPicker), {
349
+ "model-value": point.value,
350
+ tiles: __props.tiles,
351
+ "onUpdate:modelValue": onPick
352
+ }, null, 8, ["model-value", "tiles"])
353
+ ]),
354
+ createElementVNode("p", {
355
+ class: normalizeClass(["mt-3 min-h-5 text-sm", resolved.value || resolving.value ? "text-ink" : "text-muted"])
356
+ }, toDisplayString(preview.value), 3)
357
+ ]),
358
+ _: 3
359
+ }, 8, ["open", "title"])
360
+ ]);
361
+ };
362
+ }
363
+ });
364
+ export {
365
+ _sfc_main as _
366
+ };