@leavepulse/ui 0.78.1 → 0.78.3

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.
Files changed (147) hide show
  1. package/dist/chunks/{DropzoneLinkField-CLLhd3WB.js → DropzoneLinkField-CyE7TJ5f.js} +1 -1
  2. package/dist/chunks/LpAddressInput-ft8fQOGx.js +401 -0
  3. package/dist/chunks/{LpAppShell-BKbYCHkx.js → LpAppShell-DRHoQtr-.js} +34 -17
  4. package/dist/chunks/LpAutocomplete-CrZbd0uv.js +310 -0
  5. package/dist/chunks/LpCheckbox-CCWCwD0y.js +129 -0
  6. package/dist/chunks/{LpCodeDisplay-D3BUAhV1.js → LpCodeDisplay-D59uPwRb.js} +1 -1
  7. package/dist/chunks/LpColorPicker-BONURBFN.js +574 -0
  8. package/dist/chunks/LpDatePicker-C2AOtxgF.js +171 -0
  9. package/dist/chunks/{LpDateRangePicker-CNQXdTVv.js → LpDateRangePicker-CiXdIAyh.js} +43 -35
  10. package/dist/chunks/{LpFileTree-C-rJKzEQ.js → LpFileTree-CpNurwZp.js} +2 -2
  11. package/dist/chunks/{LpFileTreeNode-BgYn4U1J.js → LpFileTreeNode-DdF8Cq-s.js} +1 -1
  12. package/dist/chunks/{LpInput-D0O6dpuA.js → LpInput-CbI7z5fQ.js} +3 -4
  13. package/dist/chunks/{LpLogInspector-BdQlX6S-.js → LpLogInspector-CcSJSGan.js} +1 -1
  14. package/dist/chunks/{LpLogViewer-umCZBapN.js → LpLogViewer-CWu49Dko.js} +2 -2
  15. package/dist/chunks/{LpModalNav-gOi1Giz1.js → LpModalNav-DI3BW5ff.js} +1 -1
  16. package/dist/chunks/LpNumberField-C8LfUz_R.js +154 -0
  17. package/dist/chunks/LpOtpInput-B1X9l9no.js +140 -0
  18. package/dist/chunks/{LpPaletteTheme-BIXpZLGe.js → LpPaletteTheme-B-xuZiKK.js} +2 -2
  19. package/dist/chunks/{LpPasswordInput-D4HPhqfV.js → LpPasswordInput-Duc_3wPv.js} +1 -1
  20. package/dist/chunks/{LpPhoneInput-BSL7NX4z.js → LpPhoneInput-CctcRK9K.js} +124 -118
  21. package/dist/chunks/LpRadioGroup-DTM0nd9G.js +103 -0
  22. package/dist/chunks/LpRangeSlider-DJsEZXad.js +97 -0
  23. package/dist/chunks/LpRepeater-Czib3rmv.js +193 -0
  24. package/dist/chunks/{LpSecretField-B2roc4Iz.js → LpSecretField-BtlCk3IH.js} +1 -1
  25. package/dist/chunks/LpSegmented-sskrJKJJ.js +128 -0
  26. package/dist/chunks/LpSelect-JFZehOkd.js +515 -0
  27. package/dist/chunks/{LpSidebar-DDNJzb69.js → LpSidebar-CvRvVBTt.js} +1 -1
  28. package/dist/chunks/LpSlider-C4ilCtZX.js +76 -0
  29. package/dist/chunks/LpSwitch-DyAYTchz.js +122 -0
  30. package/dist/chunks/{LpTableFilter-B_XMOeAh.js → LpTableFilter-CJg5eIcV.js} +2 -2
  31. package/dist/chunks/{LpTagsInput-Dt0mteSj.js → LpTagsInput-CkNxpNYa.js} +4 -3
  32. package/dist/chunks/{LpTextarea-Co0b9PVx.js → LpTextarea-CFK_5QB8.js} +3 -2
  33. package/dist/chunks/{LpThemeStudio-ChU0_Ga8.js → LpThemeStudio-R-wohbCY.js} +2 -2
  34. package/dist/chunks/{LpThemeSwitcher-DmCZ21P_.js → LpThemeSwitcher-Bzy15MCP.js} +1 -1
  35. package/dist/chunks/LpTimeField-B5n0m8m0.js +115 -0
  36. package/dist/chunks/{LpTopologyCanvas-jqz1KCq2.js → LpTopologyCanvas-BNgw9RyZ.js} +2 -2
  37. package/dist/chunks/{LpWorkspaceSwitcher-BeNHCLd5.js → LpWorkspaceSwitcher-ugv-fyig.js} +1 -1
  38. package/dist/components/DropzoneLinkField.vue.js +1 -1
  39. package/dist/components/LpAddressInput.vue.d.ts +4 -0
  40. package/dist/components/LpAddressInput.vue.js +1 -1
  41. package/dist/components/LpAlert.vue.d.ts +3 -3
  42. package/dist/components/LpAppShell.vue.d.ts +19 -1
  43. package/dist/components/LpAppShell.vue.js +1 -1
  44. package/dist/components/LpAutocomplete.vue.d.ts +4 -0
  45. package/dist/components/LpAutocomplete.vue.js +1 -1
  46. package/dist/components/LpBadge.vue.d.ts +2 -2
  47. package/dist/components/LpChart.vue.d.ts +1 -1
  48. package/dist/components/LpCheckbox.vue.d.ts +4 -0
  49. package/dist/components/LpCheckbox.vue.js +1 -1
  50. package/dist/components/LpCodeDisplay.vue.js +1 -1
  51. package/dist/components/LpCollapse.vue.d.ts +1 -1
  52. package/dist/components/LpColorPicker.vue.d.ts +5 -3
  53. package/dist/components/LpColorPicker.vue.js +1 -1
  54. package/dist/components/LpDatePicker.vue.js +1 -1
  55. package/dist/components/LpDateRangePicker.vue.js +1 -1
  56. package/dist/components/LpDropzone.vue.js +1 -1
  57. package/dist/components/LpFileTree.vue.js +1 -1
  58. package/dist/components/LpFileTreeNode.vue.js +1 -1
  59. package/dist/components/LpImageEditor.vue.d.ts +1 -1
  60. package/dist/components/LpInput.vue.js +1 -1
  61. package/dist/components/LpLogInspector.vue.js +1 -1
  62. package/dist/components/LpLogViewer.vue.js +1 -1
  63. package/dist/components/LpModalNav.vue.js +1 -1
  64. package/dist/components/LpNumberField.vue.d.ts +4 -0
  65. package/dist/components/LpNumberField.vue.js +1 -1
  66. package/dist/components/LpOtpInput.vue.d.ts +4 -0
  67. package/dist/components/LpOtpInput.vue.js +1 -1
  68. package/dist/components/LpPaletteTheme.vue.js +1 -1
  69. package/dist/components/LpPasswordInput.vue.js +1 -1
  70. package/dist/components/LpPhoneInput.vue.d.ts +4 -0
  71. package/dist/components/LpPhoneInput.vue.js +1 -1
  72. package/dist/components/LpRadioGroup.vue.d.ts +4 -0
  73. package/dist/components/LpRadioGroup.vue.js +1 -1
  74. package/dist/components/LpRangeSlider.vue.d.ts +4 -0
  75. package/dist/components/LpRangeSlider.vue.js +1 -1
  76. package/dist/components/LpRepeater.vue.d.ts +2 -0
  77. package/dist/components/LpRepeater.vue.js +1 -1
  78. package/dist/components/LpScrollArea.vue.d.ts +1 -1
  79. package/dist/components/LpSecretField.vue.js +1 -1
  80. package/dist/components/LpSegmented.vue.d.ts +4 -0
  81. package/dist/components/LpSegmented.vue.js +1 -1
  82. package/dist/components/LpSelect.vue.d.ts +2 -0
  83. package/dist/components/LpSelect.vue.js +1 -1
  84. package/dist/components/LpShinyText.vue.d.ts +1 -1
  85. package/dist/components/LpSidebar.vue.d.ts +1 -1
  86. package/dist/components/LpSidebar.vue.js +1 -1
  87. package/dist/components/LpSlider.vue.d.ts +4 -0
  88. package/dist/components/LpSlider.vue.js +1 -1
  89. package/dist/components/LpSwitch.vue.d.ts +4 -0
  90. package/dist/components/LpSwitch.vue.js +1 -1
  91. package/dist/components/LpTable.vue.js +2 -2
  92. package/dist/components/LpTableFilter.vue.js +1 -1
  93. package/dist/components/LpTabs.vue.d.ts +1 -1
  94. package/dist/components/LpTagsInput.vue.js +1 -1
  95. package/dist/components/LpTextarea.vue.js +1 -1
  96. package/dist/components/LpThemeStudio.vue.js +1 -1
  97. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  98. package/dist/components/LpTimeField.vue.d.ts +4 -0
  99. package/dist/components/LpTimeField.vue.js +1 -1
  100. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  101. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  102. package/dist/components/calendar.d.ts +2 -0
  103. package/dist/composables/useField.d.ts +50 -0
  104. package/dist/composables/useField.js +31 -2
  105. package/dist/composables/useMergedClass.d.ts +6 -2
  106. package/dist/composables/useMergedClass.js +6 -6
  107. package/dist/index.js +34 -34
  108. package/package.json +1 -1
  109. package/src/components/LpAddressInput.vue +8 -4
  110. package/src/components/LpAppShell.vue +56 -18
  111. package/src/components/LpAutocomplete.vue +11 -5
  112. package/src/components/LpCheckbox.vue +8 -6
  113. package/src/components/LpColorPicker.vue +7 -4
  114. package/src/components/LpDatePicker.vue +8 -4
  115. package/src/components/LpDateRangePicker.vue +8 -4
  116. package/src/components/LpInput.vue +3 -4
  117. package/src/components/LpNumberField.vue +11 -6
  118. package/src/components/LpOtpInput.vue +8 -6
  119. package/src/components/LpPhoneInput.vue +11 -6
  120. package/src/components/LpRadioGroup.vue +9 -5
  121. package/src/components/LpRangeSlider.vue +8 -4
  122. package/src/components/LpRepeater.vue +16 -4
  123. package/src/components/LpSegmented.vue +8 -4
  124. package/src/components/LpSelect.vue +8 -7
  125. package/src/components/LpSlider.vue +8 -4
  126. package/src/components/LpSwitch.vue +8 -5
  127. package/src/components/LpTagsInput.vue +4 -3
  128. package/src/components/LpTextarea.vue +3 -2
  129. package/src/components/LpTimeField.vue +8 -6
  130. package/src/components/calendar.ts +2 -0
  131. package/src/composables/useField.ts +52 -0
  132. package/src/composables/useMergedClass.ts +11 -5
  133. package/dist/chunks/LpAddressInput-BSS49jpm.js +0 -393
  134. package/dist/chunks/LpAutocomplete-DkY6CSyD.js +0 -302
  135. package/dist/chunks/LpCheckbox-D6RuDW4X.js +0 -124
  136. package/dist/chunks/LpColorPicker-COHzV68R.js +0 -567
  137. package/dist/chunks/LpDatePicker-DwSDY0mt.js +0 -163
  138. package/dist/chunks/LpNumberField-Csg-1Zh_.js +0 -148
  139. package/dist/chunks/LpOtpInput-BRpLBbOD.js +0 -132
  140. package/dist/chunks/LpRadioGroup-BrazjLY_.js +0 -95
  141. package/dist/chunks/LpRangeSlider-fz61tzc-.js +0 -89
  142. package/dist/chunks/LpRepeater-BkJHSPI1.js +0 -180
  143. package/dist/chunks/LpSegmented-DbEJhbv-.js +0 -123
  144. package/dist/chunks/LpSelect-Y-iuSymW.js +0 -512
  145. package/dist/chunks/LpSlider-A4ixovgS.js +0 -68
  146. package/dist/chunks/LpSwitch-DnC2-0aV.js +0 -115
  147. package/dist/chunks/LpTimeField-DeOcKWJq.js +0 -110
@@ -6,6 +6,7 @@ export default { inheritAttrs: false }
6
6
  </script>
7
7
 
8
8
  <script setup lang="ts">
9
+ import FieldWrapper from "./FieldWrapper.vue"
9
10
  import { CONTROL_SIZE } from "./controlSize"
10
11
  import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
11
12
  /*
@@ -22,7 +23,7 @@ import { usePaletteControl, type PaletteControlOptions } from "../composables/us
22
23
  import { Time } from "@internationalized/date"
23
24
  import { TimeFieldInput, TimeFieldRoot } from "reka-ui"
24
25
  import { computed } from "vue"
25
- import { focusLeft, useFieldControl, mergeDescribedBy } from "../composables/useField"
26
+ import { focusLeft, useOwnField, mergeDescribedBy } from "../composables/useField"
26
27
  import { useMergedAttrs } from "../composables/useMergedClass"
27
28
  import LpIcon from "./LpIcon.vue"
28
29
 
@@ -34,6 +35,8 @@ const props = withDefaults(
34
35
  modelValue?: string
35
36
  disabled?: boolean
36
37
  invalid?: boolean
38
+ /** A message under the control, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
39
+ error?: string
37
40
  size?: "sm" | "md" | "lg"
38
41
  /** 24 or 12-hour clock, independent of the browser locale. */
39
42
  hourCycle?: 12 | 24
@@ -68,9 +71,7 @@ function onUpdate(next: unknown) {
68
71
  emit("update:modelValue", `${pad(next.hour)}:${pad(next.minute)}`)
69
72
  }
70
73
 
71
- const field = useFieldControl(() => ({
72
- wrapped: false,
73
- invalid: props.invalid,
74
+ const { field, wrapper, ownAttrs } = useOwnField(() => ({ error: props.error, invalid: props.invalid,
74
75
  disabled: props.disabled,
75
76
  }))
76
77
 
@@ -82,13 +83,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
82
83
  "flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
83
84
  shellSize[props.size],
84
85
  field.value.invalid ? "border-danger focus-within:ring-danger-soft" : "border-line focus-within:border-brand",
85
- ].join(" "),
86
- )
86
+ ].join(" "), () => ownAttrs())
87
87
 
88
88
  usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.value.invalid })
89
89
  </script>
90
90
 
91
91
  <template>
92
+ <FieldWrapper v-bind="wrapper">
92
93
  <TimeFieldRoot
93
94
  v-slot="{ segments }"
94
95
  :model-value="value"
@@ -121,4 +122,5 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
121
122
  </TimeFieldInput>
122
123
  </template>
123
124
  </TimeFieldRoot>
125
+ </FieldWrapper>
124
126
  </template>
@@ -52,6 +52,8 @@ export interface PickerFieldProps {
52
52
  clearable?: boolean
53
53
  disabled?: boolean
54
54
  invalid?: boolean
55
+ /** A message under the picker, as on LpInput: it marks it invalid and shows under it (what `form.field()` hands it). */
56
+ error?: string
55
57
  size?: "sm" | "md" | "lg"
56
58
  /** Locale of the shown date and the calendar; defaults to reka's ConfigProvider, then the browser. */
57
59
  locale?: string
@@ -154,6 +154,58 @@ export function fieldWrapperBinds(
154
154
  return { wrapped, id: field.id, label, hint, error, required, class: attrs.class, style: attrs.style }
155
155
  }
156
156
 
157
+ /**
158
+ * Whether a control with these props draws its own LpFormField. A label or a
159
+ * hint is its own; an error alone is, except inside an outer LpFormField, which
160
+ * owns the label (its `for` points at the control) and the message line: a
161
+ * second field in it showed the message twice and took the label's control id.
162
+ * There the error only marks the control invalid.
163
+ */
164
+ export function wrapsItself(
165
+ own: { label?: string; hint?: string; error?: string },
166
+ outer: ComputedRef<FieldContext> | null,
167
+ ): boolean {
168
+ if (own.label || own.hint) return true
169
+ return !!own.error && !outer?.value
170
+ }
171
+
172
+ /**
173
+ * A control that takes `label`, `hint` and `error` the way LpInput does: given
174
+ * any of them it wraps itself in LpFormField (FieldWrapper, bound to
175
+ * `wrapper`), an error marks it invalid, and the consumer's class and style lay
176
+ * out that field instead of the control (`ownAttrs` says whether the control's
177
+ * root still takes them). Without them it is the bare control, as before. So
178
+ * `v-bind="form.field('x')"`, which hands every control `error`, shows the
179
+ * message on any of them.
180
+ * Inside an outer LpFormField an error alone does not wrap (wrapsItself).
181
+ */
182
+ export function useOwnField(
183
+ state: () => { label?: string; hint?: string; error?: string; required?: boolean; invalid?: boolean; disabled?: boolean; id?: string },
184
+ ) {
185
+ const attrs = useAttrs()
186
+ const outer = useField()
187
+ const wrapped = computed(() => wrapsItself(state(), outer))
188
+ const field = useFieldControl(() => {
189
+ const own = state()
190
+ return {
191
+ wrapped: wrapped.value,
192
+ hasMessage: !!own.error || !!own.hint,
193
+ id: own.id ?? (typeof attrs.id === "string" ? attrs.id : undefined),
194
+ invalid: !!own.invalid || !!own.error,
195
+ required: own.required,
196
+ disabled: own.disabled,
197
+ }
198
+ })
199
+ const wrapper = computed(() => fieldWrapperBinds(wrapped.value, field.value, state(), attrs))
200
+ /* The attributes for the control itself: all of them, less the class and style the wrapper takes. */
201
+ const controlAttrs = computed(() => {
202
+ if (!wrapped.value) return attrs
203
+ const { class: _class, style: _style, ...rest } = attrs
204
+ return rest
205
+ })
206
+ return { wrapped, field, wrapper, controlAttrs, ownAttrs: () => !wrapped.value }
207
+ }
208
+
157
209
  /** The state of a control that takes its id from the app and draws no wrapper of its own. */
158
210
  export function useAttrsField(state: () => { invalid?: boolean; disabled?: boolean }) {
159
211
  const attrs = useAttrs()
@@ -62,11 +62,13 @@ function merge(...classes: (string | undefined | null | false)[]): string {
62
62
  */
63
63
  export function useMergedClass(
64
64
  own: MaybeRefOrGetter<string | undefined>,
65
+ /** False while something else takes the consumer's class (a field wrapper). */
66
+ takesAttrs: () => boolean = () => true,
65
67
  ): ComputedRef<string> {
66
68
  const attrs = useAttrs()
67
69
  // `cn` returns undefined when everything it was given was empty; a class
68
70
  // binding is happier with "" than with undefined.
69
- return computed(() => merge(toValue(own), attrs.class as string | undefined))
71
+ return computed(() => merge(toValue(own), takesAttrs() ? (attrs.class as string | undefined) : undefined))
70
72
  }
71
73
 
72
74
  /**
@@ -85,16 +87,20 @@ export function useMergedClass(
85
87
  * Vue appends that second `class` to the merged one, and stylesheet order —
86
88
  * not the caller — decides which wins.
87
89
  */
88
- export function useMergedAttrs(own: MaybeRefOrGetter<string | undefined>): {
90
+ export function useMergedAttrs(
91
+ own: MaybeRefOrGetter<string | undefined>,
92
+ /** False while a field wrapper takes the consumer's class and style (useOwnField's `ownAttrs`). */
93
+ takesAttrs: () => boolean = () => true,
94
+ ): {
89
95
  class: ComputedRef<string>
90
96
  attrs: ComputedRef<Record<string, unknown>>
91
97
  } {
92
98
  const attrs = useAttrs()
93
99
  return {
94
- class: useMergedClass(own),
100
+ class: useMergedClass(own, takesAttrs),
95
101
  attrs: computed(() => {
96
- const { class: _omitted, ...rest } = attrs
97
- return rest
102
+ const { class: _omitted, style, ...rest } = attrs
103
+ return takesAttrs() ? { ...rest, ...(style === undefined ? {} : { style }) } : rest
98
104
  }),
99
105
  }
100
106
  }
@@ -1,393 +0,0 @@
1
- import { defineComponent, defineAsyncComponent, computed, watch, ref, onMounted, openBlock, createElementBlock, mergeProps, unref, createVNode, withCtx, createElementVNode, createBlock, createCommentVNode, createTextVNode, toDisplayString, renderSlot, normalizeClass } from "vue";
2
- import { useAsyncTask } from "../composables/useAsyncTask.js";
3
- import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress } from "../components/address.js";
4
- import { flagEmoji, loadCountries } from "../components/countries.js";
5
- import { _ as _sfc_main$2 } from "./LpAutocomplete-DkY6CSyD.js";
6
- import { _ as _sfc_main$3 } from "./LpButton-DSHioIfD.js";
7
- import { _ as _sfc_main$1 } from "./LpFormField-BKYn9dyP.js";
8
- import { _ as _sfc_main$4 } from "./LpIcon-C6JpQ5td.js";
9
- import { _ as _sfc_main$5 } from "./LpInput-D0O6dpuA.js";
10
- import { _ as _sfc_main$7 } from "./LpModal-DDvrSAnf.js";
11
- import { _ as _sfc_main$6 } from "./LpSelect-Y-iuSymW.js";
12
- import { useFieldControl, mergeDescribedBy } from "../composables/useField.js";
13
- import { useLabels } from "../composables/useLpConfig.js";
14
- import { useMergedAttrs } from "../composables/useMergedClass.js";
15
- const _hoisted_1 = ["aria-labelledby", "aria-invalid", "aria-describedby"];
16
- const _hoisted_2 = { class: "flex items-stretch gap-2" };
17
- const _hoisted_3 = { class: "grid gap-3 sm:grid-cols-[minmax(0,1fr)_10rem]" };
18
- const _hoisted_4 = {
19
- key: 1,
20
- class: "flex items-center gap-2"
21
- };
22
- const _hoisted_5 = {
23
- key: 0,
24
- class: "text-xs text-muted"
25
- };
26
- const __default__ = { inheritAttrs: false };
27
- const _sfc_main = /* @__PURE__ */ defineComponent({
28
- ...__default__,
29
- __name: "LpAddressInput",
30
- props: {
31
- modelValue: {},
32
- provider: {},
33
- tiles: { default: () => OSM_TILES },
34
- only: {},
35
- secondLine: { type: Boolean },
36
- minChars: { default: 3 },
37
- debounce: { default: 350 },
38
- language: {},
39
- disabled: { type: Boolean },
40
- invalid: { type: Boolean },
41
- size: { default: "md" },
42
- hideMap: { type: Boolean },
43
- labels: {}
44
- },
45
- emits: ["update:modelValue", "pick"],
46
- setup(__props, { emit: __emit }) {
47
- const LpMapPicker = defineAsyncComponent(() => import("../components/LpMapPicker.vue.js"));
48
- const props = __props;
49
- const emit = __emit;
50
- const DEFAULT_LABELS = {
51
- line: "Street and number",
52
- linePlaceholder: "Baker Street 221B",
53
- secondLine: "Apartment, suite (optional)",
54
- secondLinePlaceholder: "Flat 4",
55
- city: "City",
56
- cityPlaceholder: "London",
57
- postalCode: "Postcode",
58
- postalCodePlaceholder: "NW1 6XE",
59
- country: "Country",
60
- countryPlaceholder: "Select country…",
61
- countrySearch: "Search country…",
62
- pickOnMap: "Pick on map",
63
- mapTitle: "Pick a location",
64
- mapHint: "Click the map to drop a pin, drag to move around.",
65
- use: "Use this address",
66
- cancel: "Cancel",
67
- searching: "Searching…",
68
- noSuggestions: "No suggestions",
69
- nothingHere: "No address found here"
70
- };
71
- const text = useLabels("addressInput", DEFAULT_LABELS, () => props.labels);
72
- const fallbackProvider = createNominatimProvider();
73
- const geocoder = computed(() => props.provider ?? fallbackProvider);
74
- const value = computed(() => props.modelValue ?? emptyAddress());
75
- let latest;
76
- watch(
77
- () => props.modelValue,
78
- () => latest = void 0
79
- );
80
- function patch(changes) {
81
- latest = { ...latest ?? value.value, ...changes };
82
- emit("update:modelValue", latest);
83
- }
84
- function edit(changes) {
85
- patch({ ...changes, lat: void 0, lon: void 0 });
86
- }
87
- const allCountries = ref([]);
88
- const countries = computed(() => {
89
- const only = props.only?.map((code) => code.toUpperCase());
90
- return only?.length ? allCountries.value.filter((c) => only.includes(c.iso2)) : allCountries.value;
91
- });
92
- const countryOptions = computed(
93
- () => countries.value.map((country) => ({
94
- value: country.iso2,
95
- label: `${flagEmoji(country.iso2)} ${country.name}`
96
- }))
97
- );
98
- onMounted(async () => {
99
- allCountries.value = await loadCountries();
100
- });
101
- function useSuggestions(field2) {
102
- const options = ref([]);
103
- const task = useAsyncTask();
104
- function query(term) {
105
- if (term.trim().length < props.minChars) {
106
- task.cancel();
107
- options.value = [];
108
- return;
109
- }
110
- task.run(
111
- (signal) => geocoder.value.search(term.trim(), field2, {
112
- country: value.value.country || void 0,
113
- language: props.language,
114
- signal
115
- }),
116
- {
117
- delay: props.debounce,
118
- apply: (found) => options.value = found,
119
- // Network failure: leave the field usable as free text.
120
- fail: () => options.value = []
121
- }
122
- );
123
- }
124
- return { options, loading: task.pending, query };
125
- }
126
- const lineSuggestions = useSuggestions("line");
127
- const citySuggestions = useSuggestions("city");
128
- function applySuggestion(suggestion, keep) {
129
- const parsed = suggestion.address;
130
- const changes = { lat: suggestion.lat, lon: suggestion.lon };
131
- if (parsed.line && keep !== "line") changes.line = parsed.line;
132
- if (parsed.city) changes.city = parsed.city;
133
- if (parsed.postalCode) changes.postalCode = parsed.postalCode;
134
- if (parsed.country) changes.country = parsed.country;
135
- patch(changes);
136
- }
137
- function onLineSelect(chosen) {
138
- const suggestion = lineSuggestions.options.value.find((option) => option.value === chosen);
139
- if (suggestion) applySuggestion(suggestion, "line");
140
- }
141
- function onCitySelect(chosen) {
142
- const suggestion = citySuggestions.options.value.find((option) => option.value === chosen);
143
- if (suggestion) applySuggestion(suggestion, "city");
144
- }
145
- const mapOpen = ref(false);
146
- const point = ref();
147
- const resolved = ref();
148
- const resolving = ref(false);
149
- computed(
150
- () => countries.value.find((c) => c.iso2 === value.value.country)?.name
151
- );
152
- const preview = computed(() => {
153
- if (resolving.value) return text.value.searching;
154
- if (!point.value) return text.value.mapHint;
155
- if (!resolved.value) return text.value.nothingHere;
156
- const parsed = resolved.value.address;
157
- return formatAddress(
158
- {
159
- line: parsed.line ?? "",
160
- city: parsed.city ?? "",
161
- postalCode: parsed.postalCode ?? "",
162
- country: parsed.country ?? ""
163
- },
164
- countries.value.find((c) => c.iso2 === parsed.country)?.name
165
- );
166
- });
167
- watch(mapOpen, (open) => {
168
- if (!open) return;
169
- point.value = value.value.lat != null && value.value.lon != null ? { lat: value.value.lat, lon: value.value.lon } : void 0;
170
- resolved.value = void 0;
171
- });
172
- const reverse = useAsyncTask();
173
- watch(reverse.pending, (busy) => resolving.value = busy);
174
- function onPick(next) {
175
- point.value = next;
176
- resolved.value = void 0;
177
- reverse.run(
178
- () => geocoder.value.reverse(next.lat, next.lon, { language: props.language }),
179
- {
180
- apply: (found) => resolved.value = found,
181
- fail: () => resolved.value = void 0
182
- }
183
- );
184
- }
185
- function confirmPoint() {
186
- if (!point.value) return;
187
- const parsed = resolved.value?.address ?? {};
188
- patch({
189
- line: parsed.line ?? value.value.line,
190
- city: parsed.city ?? value.value.city,
191
- postalCode: parsed.postalCode ?? value.value.postalCode,
192
- country: parsed.country ?? value.value.country,
193
- lat: point.value.lat,
194
- lon: point.value.lon
195
- });
196
- emit("pick", point.value);
197
- mapOpen.value = false;
198
- }
199
- const field = useFieldControl(() => ({ wrapped: false, invalid: props.invalid, disabled: props.disabled }));
200
- const { class: rootClass, attrs: rest } = useMergedAttrs(
201
- "flex flex-col gap-3"
202
- );
203
- return (_ctx, _cache) => {
204
- return openBlock(), createElementBlock("div", mergeProps({
205
- role: "group",
206
- "aria-labelledby": unref(field).labelId,
207
- "aria-invalid": unref(field).invalid || void 0,
208
- class: unref(rootClass)
209
- }, unref(rest), {
210
- "aria-describedby": unref(mergeDescribedBy)(unref(rest)["aria-describedby"], unref(field).describedBy)
211
- }), [
212
- createVNode(_sfc_main$1, {
213
- label: unref(text).line
214
- }, {
215
- default: withCtx(() => [
216
- createElementVNode("div", _hoisted_2, [
217
- createVNode(_sfc_main$2, {
218
- class: "min-w-0 flex-1",
219
- "model-value": value.value.line,
220
- options: unref(lineSuggestions).options.value,
221
- loading: unref(lineSuggestions).loading.value,
222
- filter: false,
223
- "min-chars": __props.minChars,
224
- disabled: unref(field).disabled,
225
- invalid: unref(field).invalid,
226
- size: __props.size,
227
- placeholder: unref(text).linePlaceholder,
228
- "empty-text": unref(text).noSuggestions,
229
- "loading-text": unref(text).searching,
230
- icon: "lucide:map-pin",
231
- "onUpdate:modelValue": _cache[0] || (_cache[0] = (next) => {
232
- edit({ line: next });
233
- unref(lineSuggestions).query(next);
234
- }),
235
- onSelect: onLineSelect
236
- }, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "invalid", "size", "placeholder", "empty-text", "loading-text"]),
237
- !__props.hideMap ? (openBlock(), createBlock(_sfc_main$3, {
238
- key: 0,
239
- variant: "soft",
240
- size: __props.size,
241
- square: "",
242
- disabled: unref(field).disabled,
243
- title: unref(text).pickOnMap,
244
- "aria-label": unref(text).pickOnMap,
245
- onClick: _cache[1] || (_cache[1] = ($event) => mapOpen.value = true)
246
- }, {
247
- default: withCtx(() => [
248
- createVNode(_sfc_main$4, {
249
- name: "lucide:map",
250
- size: 16
251
- })
252
- ]),
253
- _: 1
254
- }, 8, ["size", "disabled", "title", "aria-label"])) : createCommentVNode("", true)
255
- ])
256
- ]),
257
- _: 1
258
- }, 8, ["label"]),
259
- __props.secondLine ? (openBlock(), createBlock(_sfc_main$5, {
260
- key: 0,
261
- "model-value": value.value.line2 ?? "",
262
- label: unref(text).secondLine,
263
- placeholder: unref(text).secondLinePlaceholder,
264
- disabled: unref(field).disabled,
265
- invalid: unref(field).invalid,
266
- size: __props.size,
267
- autocomplete: "address-line2",
268
- "onUpdate:modelValue": _cache[2] || (_cache[2] = (next) => patch({ line2: next }))
269
- }, null, 8, ["model-value", "label", "placeholder", "disabled", "invalid", "size"])) : createCommentVNode("", true),
270
- createElementVNode("div", _hoisted_3, [
271
- createVNode(_sfc_main$1, {
272
- label: unref(text).city
273
- }, {
274
- default: withCtx(() => [
275
- createVNode(_sfc_main$2, {
276
- "model-value": value.value.city,
277
- options: unref(citySuggestions).options.value,
278
- loading: unref(citySuggestions).loading.value,
279
- filter: false,
280
- "min-chars": __props.minChars,
281
- disabled: unref(field).disabled,
282
- invalid: unref(field).invalid,
283
- size: __props.size,
284
- placeholder: unref(text).cityPlaceholder,
285
- "empty-text": unref(text).noSuggestions,
286
- "loading-text": unref(text).searching,
287
- "onUpdate:modelValue": _cache[3] || (_cache[3] = (next) => {
288
- edit({ city: next });
289
- unref(citySuggestions).query(next);
290
- }),
291
- onSelect: onCitySelect
292
- }, null, 8, ["model-value", "options", "loading", "min-chars", "disabled", "invalid", "size", "placeholder", "empty-text", "loading-text"])
293
- ]),
294
- _: 1
295
- }, 8, ["label"]),
296
- createVNode(_sfc_main$5, {
297
- "model-value": value.value.postalCode,
298
- label: unref(text).postalCode,
299
- placeholder: unref(text).postalCodePlaceholder,
300
- disabled: unref(field).disabled,
301
- invalid: unref(field).invalid,
302
- size: __props.size,
303
- autocomplete: "postal-code",
304
- "onUpdate:modelValue": _cache[4] || (_cache[4] = (next) => edit({ postalCode: next }))
305
- }, null, 8, ["model-value", "label", "placeholder", "disabled", "invalid", "size"])
306
- ]),
307
- createVNode(_sfc_main$1, {
308
- label: unref(text).country
309
- }, {
310
- default: withCtx(() => [
311
- createVNode(_sfc_main$6, {
312
- "model-value": value.value.country,
313
- options: countryOptions.value,
314
- placeholder: unref(text).countryPlaceholder,
315
- "search-placeholder": unref(text).countrySearch,
316
- disabled: unref(field).disabled,
317
- invalid: unref(field).invalid,
318
- size: __props.size,
319
- searchable: "",
320
- "onUpdate:modelValue": _cache[5] || (_cache[5] = (next) => edit({ country: String(next ?? "") }))
321
- }, null, 8, ["model-value", "options", "placeholder", "search-placeholder", "disabled", "invalid", "size"])
322
- ]),
323
- _: 1
324
- }, 8, ["label"]),
325
- !__props.hideMap ? (openBlock(), createElementBlock("div", _hoisted_4, [
326
- createVNode(_sfc_main$3, {
327
- variant: "soft",
328
- size: __props.size === "lg" ? "md" : "sm",
329
- disabled: unref(field).disabled,
330
- onClick: _cache[6] || (_cache[6] = ($event) => mapOpen.value = true)
331
- }, {
332
- default: withCtx(() => [
333
- createVNode(_sfc_main$4, {
334
- name: "lucide:map",
335
- size: 15
336
- }),
337
- createTextVNode(" " + toDisplayString(unref(text).pickOnMap), 1)
338
- ]),
339
- _: 1
340
- }, 8, ["size", "disabled"]),
341
- 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)
342
- ])) : createCommentVNode("", true),
343
- createVNode(_sfc_main$7, {
344
- open: mapOpen.value,
345
- "onUpdate:open": _cache[8] || (_cache[8] = ($event) => mapOpen.value = $event),
346
- title: unref(text).mapTitle,
347
- size: "2xl"
348
- }, {
349
- footer: withCtx(() => [
350
- createVNode(_sfc_main$3, {
351
- variant: "ghost",
352
- onClick: _cache[7] || (_cache[7] = ($event) => mapOpen.value = false)
353
- }, {
354
- default: withCtx(() => [
355
- createTextVNode(toDisplayString(unref(text).cancel), 1)
356
- ]),
357
- _: 1
358
- }),
359
- createVNode(_sfc_main$3, {
360
- disabled: !point.value,
361
- onClick: confirmPoint
362
- }, {
363
- default: withCtx(() => [
364
- createTextVNode(toDisplayString(unref(text).use), 1)
365
- ]),
366
- _: 1
367
- }, 8, ["disabled"])
368
- ]),
369
- default: withCtx(() => [
370
- renderSlot(_ctx.$slots, "map", {
371
- point: point.value,
372
- setPoint: onPick,
373
- tiles: __props.tiles
374
- }, () => [
375
- createVNode(unref(LpMapPicker), {
376
- "model-value": point.value,
377
- tiles: __props.tiles,
378
- "onUpdate:modelValue": onPick
379
- }, null, 8, ["model-value", "tiles"])
380
- ]),
381
- createElementVNode("p", {
382
- class: normalizeClass(["mt-3 min-h-5 text-sm", resolved.value || resolving.value ? "text-ink" : "text-muted"])
383
- }, toDisplayString(preview.value), 3)
384
- ]),
385
- _: 3
386
- }, 8, ["open", "title"])
387
- ], 16, _hoisted_1);
388
- };
389
- }
390
- });
391
- export {
392
- _sfc_main as _
393
- };