@marianmeres/stuic 3.83.3 → 3.85.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.
@@ -7,9 +7,17 @@
7
7
  CheckoutValidationError,
8
8
  } from "./_internal/checkout-types.js";
9
9
  import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
10
+ import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
11
+ import type { Country } from "../Input/_internal/countries.js";
10
12
 
11
13
  export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
12
- /** Bindable address data. Default: createEmptyAddress() */
14
+ /**
15
+ * Bindable address data. Default: createEmptyAddress().
16
+ *
17
+ * Note: `address.country` is expected to be an ISO alpha-2 code (e.g. "SK")
18
+ * when used with the built-in country selector. Pre-existing free-text values
19
+ * won't match a country and will render as unselected.
20
+ */
13
21
  address?: CheckoutAddressData;
14
22
 
15
23
  /**
@@ -28,6 +36,7 @@
28
36
  name?: boolean;
29
37
  street?: boolean;
30
38
  city?: boolean;
39
+ state_or_region?: boolean;
31
40
  postal_code?: boolean;
32
41
  country?: boolean;
33
42
  phone?: boolean;
@@ -41,12 +50,13 @@
41
50
 
42
51
  /**
43
52
  * Override the country field with a custom selector.
44
- * When provided, replaces the default text input for country.
53
+ * When provided, replaces the default searchable country picker.
54
+ * `value` is an ISO alpha-2 code.
45
55
  */
46
56
  countryField?: Snippet<
47
57
  [
48
58
  {
49
- /** Current country value */
59
+ /** Current country value (ISO alpha-2 code) */
50
60
  value: string;
51
61
  /** Called when country changes */
52
62
  onchange: (value: string) => void;
@@ -60,9 +70,29 @@
60
70
  ]
61
71
  >;
62
72
 
73
+ /**
74
+ * Restrict the country selector to a specific list. Accepts ISO codes
75
+ * or already-resolved Country objects. Default: all countries.
76
+ */
77
+ countryList?: Country[] | string[];
78
+
79
+ /**
80
+ * ISO codes pinned at the top of the country dropdown, above a divider.
81
+ */
82
+ preferredCountries?: string[];
83
+
84
+ /**
85
+ * Override displayed country names. Keys are ISO alpha-2 codes,
86
+ * values are the localized name. Missing keys fall back to English.
87
+ */
88
+ countryNames?: Record<string, string>;
89
+
63
90
  /** Extra props forwarded to the internal FieldPhoneNumber component. */
64
91
  phoneFieldProps?: Partial<FieldPhoneNumberProps>;
65
92
 
93
+ /** Extra props forwarded to the internal FieldCountry component. */
94
+ countryFieldProps?: Partial<FieldCountryProps>;
95
+
66
96
  t?: TranslateFn;
67
97
  unstyled?: boolean;
68
98
  class?: string;
@@ -76,6 +106,7 @@
76
106
  import { createEmptyAddress } from "./_internal/checkout-utils.js";
77
107
  import FieldInput from "../Input/FieldInput.svelte";
78
108
  import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
109
+ import FieldCountry from "../Input/FieldCountry.svelte";
79
110
  import { validatePhoneNumber } from "../Input/phone-validation.js";
80
111
 
81
112
  const DEFAULT_REQUIRED = ["name", "street", "city", "postal_code", "country"];
@@ -87,7 +118,11 @@
87
118
  fields,
88
119
  requiredFields = DEFAULT_REQUIRED,
89
120
  countryField,
121
+ countryList,
122
+ preferredCountries,
123
+ countryNames,
90
124
  phoneFieldProps,
125
+ countryFieldProps,
91
126
  t: tProp,
92
127
  unstyled = false,
93
128
  class: classProp,
@@ -163,8 +198,8 @@
163
198
  />
164
199
  {/if}
165
200
 
166
- <!-- City + Postal Code (2-column grid) -->
167
- {#if fields?.city !== false || fields?.postal_code !== false}
201
+ <!-- City + State/Region + Postal Code (responsive 2- or 3-column grid) -->
202
+ {#if fields?.city !== false || fields?.state_or_region !== false || fields?.postal_code !== false}
168
203
  <div class={unstyled ? undefined : "stuic-checkout-address-row"}>
169
204
  {#if fields?.city !== false}
170
205
  <!-- svelte-ignore binding_property_non_reactive -->
@@ -183,6 +218,23 @@
183
218
  }}
184
219
  />
185
220
  {/if}
221
+ {#if fields?.state_or_region !== false}
222
+ <!-- svelte-ignore binding_property_non_reactive -->
223
+ <FieldInput
224
+ bind:value={address.state_or_region}
225
+ label={t("checkout.address.state_or_region_label")}
226
+ labelLeftBreakpoint={0}
227
+ placeholder={t("checkout.address.state_or_region_placeholder")}
228
+ required={isRequired("state_or_region")}
229
+ name="{label}-state_or_region"
230
+ id="{label}-state_or_region"
231
+ validate={{
232
+ customValidator(val) {
233
+ return fieldError("state_or_region") || "";
234
+ },
235
+ }}
236
+ />
237
+ {/if}
186
238
  {#if fields?.postal_code !== false}
187
239
  <!-- svelte-ignore binding_property_non_reactive -->
188
240
  <FieldInput
@@ -217,7 +269,7 @@
217
269
  })}
218
270
  {:else}
219
271
  <!-- svelte-ignore binding_property_non_reactive -->
220
- <FieldInput
272
+ <FieldCountry
221
273
  bind:value={address.country}
222
274
  label={t("checkout.address.country_label")}
223
275
  placeholder={t("checkout.address.country_placeholder")}
@@ -225,11 +277,16 @@
225
277
  name="{label}-country"
226
278
  id="{label}-country"
227
279
  labelLeftBreakpoint={0}
280
+ {countryList}
281
+ {preferredCountries}
282
+ {countryNames}
283
+ t={tProp}
228
284
  validate={{
229
285
  customValidator(val) {
230
286
  return fieldError("country") || "";
231
287
  },
232
288
  }}
289
+ {...countryFieldProps}
233
290
  />
234
291
  {/if}
235
292
  {/if}
@@ -238,7 +295,9 @@
238
295
  {#if fields?.phone !== false}
239
296
  <FieldPhoneNumber
240
297
  value={address.phone ?? ""}
241
- onChange={(v) => { address.phone = v; }}
298
+ onChange={(v) => {
299
+ address.phone = v;
300
+ }}
242
301
  label={t("checkout.address.phone_label")}
243
302
  placeholder={t("checkout.address.phone_placeholder")}
244
303
  required={isRequired("phone")}
@@ -3,8 +3,16 @@ import type { HTMLAttributes } from "svelte/elements";
3
3
  import type { TranslateFn } from "../../types.js";
4
4
  import type { CheckoutAddressData, CheckoutValidationError } from "./_internal/checkout-types.js";
5
5
  import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
6
+ import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
7
+ import type { Country } from "../Input/_internal/countries.js";
6
8
  export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
7
- /** Bindable address data. Default: createEmptyAddress() */
9
+ /**
10
+ * Bindable address data. Default: createEmptyAddress().
11
+ *
12
+ * Note: `address.country` is expected to be an ISO alpha-2 code (e.g. "SK")
13
+ * when used with the built-in country selector. Pre-existing free-text values
14
+ * won't match a country and will render as unselected.
15
+ */
8
16
  address?: CheckoutAddressData;
9
17
  /**
10
18
  * Label prefix used for:
@@ -20,6 +28,7 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
20
28
  name?: boolean;
21
29
  street?: boolean;
22
30
  city?: boolean;
31
+ state_or_region?: boolean;
23
32
  postal_code?: boolean;
24
33
  country?: boolean;
25
34
  phone?: boolean;
@@ -31,11 +40,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
31
40
  requiredFields?: string[];
32
41
  /**
33
42
  * Override the country field with a custom selector.
34
- * When provided, replaces the default text input for country.
43
+ * When provided, replaces the default searchable country picker.
44
+ * `value` is an ISO alpha-2 code.
35
45
  */
36
46
  countryField?: Snippet<[
37
47
  {
38
- /** Current country value */
48
+ /** Current country value (ISO alpha-2 code) */
39
49
  value: string;
40
50
  /** Called when country changes */
41
51
  onchange: (value: string) => void;
@@ -47,8 +57,24 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
47
57
  id: string;
48
58
  }
49
59
  ]>;
60
+ /**
61
+ * Restrict the country selector to a specific list. Accepts ISO codes
62
+ * or already-resolved Country objects. Default: all countries.
63
+ */
64
+ countryList?: Country[] | string[];
65
+ /**
66
+ * ISO codes pinned at the top of the country dropdown, above a divider.
67
+ */
68
+ preferredCountries?: string[];
69
+ /**
70
+ * Override displayed country names. Keys are ISO alpha-2 codes,
71
+ * values are the localized name. Missing keys fall back to English.
72
+ */
73
+ countryNames?: Record<string, string>;
50
74
  /** Extra props forwarded to the internal FieldPhoneNumber component. */
51
75
  phoneFieldProps?: Partial<FieldPhoneNumberProps>;
76
+ /** Extra props forwarded to the internal FieldCountry component. */
77
+ countryFieldProps?: Partial<FieldCountryProps>;
52
78
  t?: TranslateFn;
53
79
  unstyled?: boolean;
54
80
  class?: string;
@@ -22,6 +22,10 @@
22
22
  grid-template-columns: 1fr;
23
23
  }
24
24
 
25
+ .stuic-checkout-address-row:has(> :nth-child(3)) {
26
+ grid-template-columns: repeat(3, 1fr);
27
+ }
28
+
25
29
  .stuic-checkout-guest-form[data-small] .stuic-checkout-guest-row,
26
30
  .stuic-checkout-address[data-small] .stuic-checkout-address-row {
27
31
  grid-template-columns: 1fr;
@@ -64,10 +64,14 @@ const DEFAULTS = {
64
64
  "checkout.address.street_placeholder": "",
65
65
  "checkout.address.city_label": "City",
66
66
  "checkout.address.city_placeholder": "",
67
+ "checkout.address.state_or_region_label": "State / Region",
68
+ "checkout.address.state_or_region_placeholder": "",
67
69
  "checkout.address.postal_code_label": "Postal Code",
68
70
  "checkout.address.postal_code_placeholder": "",
69
71
  "checkout.address.country_label": "Country",
70
72
  "checkout.address.country_placeholder": "",
73
+ "checkout.address.country_search_placeholder": "Search country...",
74
+ "checkout.address.country_no_results": "No country found",
71
75
  "checkout.address.phone_label": "Phone",
72
76
  "checkout.address.phone_placeholder": "",
73
77
  "checkout.address.required_marker": "*",
@@ -14,6 +14,7 @@ export interface CheckoutAddressData {
14
14
  name: string;
15
15
  street: string;
16
16
  city: string;
17
+ state_or_region?: string;
17
18
  postal_code: string;
18
19
  country: string;
19
20
  phone?: string;
@@ -48,6 +48,7 @@ const ALL_ADDRESS_FIELDS = [
48
48
  "name",
49
49
  "street",
50
50
  "city",
51
+ "state_or_region",
51
52
  "postal_code",
52
53
  "country",
53
54
  "phone",
@@ -106,7 +107,15 @@ export function validateLoginForm(data, t) {
106
107
  // Empty data factories
107
108
  // ====================================================================
108
109
  export function createEmptyAddress() {
109
- return { name: "", street: "", city: "", postal_code: "", country: "", phone: "" };
110
+ return {
111
+ name: "",
112
+ street: "",
113
+ city: "",
114
+ state_or_region: "",
115
+ postal_code: "",
116
+ country: "",
117
+ phone: "",
118
+ };
110
119
  }
111
120
  export function createEmptyCustomerFormData() {
112
121
  return {
@@ -0,0 +1,326 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type { THC } from "../Thc/Thc.svelte";
6
+ import type { Country } from "./_internal/countries.js";
7
+ import type { InputWrapClassProps } from "./types.js";
8
+
9
+ type SnippetWithId = Snippet<[{ id: string }]>;
10
+
11
+ export interface Props extends InputWrapClassProps, Record<string, any> {
12
+ /** Selected country ISO alpha-2 code (e.g. "SK"). Bindable. Empty = unselected. */
13
+ value?: string;
14
+
15
+ /** Called whenever the selection changes. */
16
+ onChange?: (iso: string) => void;
17
+
18
+ /**
19
+ * Restrict the list to specific countries. Accepts either ISO codes or
20
+ * already-resolved Country objects. Default: all COUNTRIES.
21
+ */
22
+ countryList?: Country[] | string[];
23
+
24
+ /** ISO codes to pin at the top of the dropdown, above a divider. */
25
+ preferredCountries?: string[];
26
+
27
+ /**
28
+ * Override displayed country names. Keys are ISO alpha-2 codes,
29
+ * values are the localized name. Missing keys fall back to the English
30
+ * name from countries.ts.
31
+ */
32
+ countryNames?: Record<string, string>;
33
+
34
+ /** Show country flag emoji in dropdown items. Default: true. */
35
+ flags?: boolean;
36
+
37
+ /** Hidden input name (enables form submission + validation). */
38
+ name?: string;
39
+ id?: string;
40
+ tabindex?: number;
41
+ placeholder?: string;
42
+ required?: boolean;
43
+ disabled?: boolean;
44
+
45
+ label?: SnippetWithId | THC;
46
+ description?: SnippetWithId | THC;
47
+ class?: string;
48
+ renderSize?: "sm" | "md" | "lg" | string;
49
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
50
+
51
+ labelAfter?: SnippetWithId | THC;
52
+ inputBefore?: SnippetWithId | THC;
53
+ inputAfter?: SnippetWithId | THC;
54
+ inputBelow?: SnippetWithId | THC;
55
+ below?: SnippetWithId | THC;
56
+ labelLeft?: boolean;
57
+ labelLeftWidth?: "normal" | "wide";
58
+ labelLeftBreakpoint?: number;
59
+
60
+ /** Classes for the underlying trigger <button> element. */
61
+ classInput?: string;
62
+ /** Classes for the dropdown popover. */
63
+ classDropdown?: string;
64
+ style?: string;
65
+
66
+ t?: TranslateFn;
67
+ }
68
+ </script>
69
+
70
+ <script lang="ts">
71
+ import {
72
+ validate as validateAction,
73
+ type ValidationResult,
74
+ } from "../../actions/validate.svelte.js";
75
+ import { getId } from "../../utils/get-id.js";
76
+ import { twMerge } from "../../utils/tw-merge.js";
77
+ import { iconChevronDown } from "../../icons/index.js";
78
+ import DropdownMenu, {
79
+ type DropdownMenuActionItem,
80
+ type DropdownMenuItem,
81
+ type DropdownMenuSearchConfig,
82
+ } from "../DropdownMenu/DropdownMenu.svelte";
83
+ import InputWrap from "./_internal/InputWrap.svelte";
84
+ import { COUNTRIES, ISO_MAP } from "./_internal/countries.js";
85
+
86
+ let {
87
+ value = $bindable(""),
88
+ onChange,
89
+ countryList: countryListProp,
90
+ preferredCountries,
91
+ countryNames,
92
+ flags = true,
93
+ //
94
+ name,
95
+ id = getId(),
96
+ tabindex = 0,
97
+ placeholder,
98
+ required = false,
99
+ disabled = false,
100
+ //
101
+ label,
102
+ description,
103
+ class: classProp,
104
+ renderSize = "md",
105
+ validate,
106
+ //
107
+ labelAfter,
108
+ inputBefore,
109
+ inputAfter,
110
+ inputBelow,
111
+ below,
112
+ labelLeft = false,
113
+ labelLeftWidth = "normal",
114
+ labelLeftBreakpoint = 480,
115
+ //
116
+ classInput,
117
+ classDropdown,
118
+ classLabel,
119
+ classLabelBox,
120
+ classInputBox,
121
+ classInputBoxWrap,
122
+ classInputBoxWrapInvalid,
123
+ classDescBox,
124
+ classDescBoxToggle,
125
+ classBelowBox,
126
+ classValidationBox,
127
+ style,
128
+ //
129
+ t,
130
+ ...rest
131
+ }: Props = $props();
132
+
133
+ let isOpen = $state(false);
134
+ let hiddenInputEl: HTMLInputElement | undefined = $state();
135
+ let validation: ValidationResult | undefined = $state();
136
+ const setValidationResult = (res: ValidationResult) => (validation = res);
137
+
138
+ function localizedName(c: Country): string {
139
+ return countryNames?.[c.iso] ?? c.name;
140
+ }
141
+
142
+ // Resolve the working country list (accept ISO codes or Country objects).
143
+ let resolvedCountries: Country[] = $derived.by(() => {
144
+ if (!countryListProp) return COUNTRIES;
145
+ if (countryListProp.length === 0) return [];
146
+ if (typeof countryListProp[0] === "string") {
147
+ const set = new Set(
148
+ (countryListProp as string[]).map((c) => c.toUpperCase())
149
+ );
150
+ return COUNTRIES.filter((c) => set.has(c.iso));
151
+ }
152
+ return countryListProp as Country[];
153
+ });
154
+
155
+ // Sort resolved list alphabetically by displayed name (locale-aware).
156
+ let sortedCountries: Country[] = $derived(
157
+ [...resolvedCountries].sort((a, b) =>
158
+ localizedName(a).localeCompare(localizedName(b))
159
+ )
160
+ );
161
+
162
+ let selectedCountry: Country | undefined = $derived(
163
+ value ? ISO_MAP.get(value.toUpperCase()) : undefined
164
+ );
165
+
166
+ function countryToItem(c: Country): DropdownMenuActionItem {
167
+ const name = localizedName(c);
168
+ const prefix = flags ? `${c.flag} ` : "";
169
+ return {
170
+ type: "action",
171
+ id: c.iso,
172
+ label: `${prefix}${name}`,
173
+ onSelect: () => {
174
+ value = c.iso;
175
+ onChange?.(c.iso);
176
+ // Trigger change on hidden input so validation runs.
177
+ hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
178
+ },
179
+ };
180
+ }
181
+
182
+ let items: DropdownMenuItem[] = $derived.by(() => {
183
+ const result: DropdownMenuItem[] = [];
184
+ const preferredSet = new Set(
185
+ preferredCountries?.map((c) => c.toUpperCase()) ?? []
186
+ );
187
+
188
+ if (preferredSet.size > 0) {
189
+ // Preserve the order given in `preferredCountries`.
190
+ const order = preferredCountries!.map((c) => c.toUpperCase());
191
+ const preferred = order
192
+ .map((iso) => resolvedCountries.find((c) => c.iso === iso))
193
+ .filter((c): c is Country => !!c);
194
+ preferred.forEach((c) => result.push(countryToItem(c)));
195
+ if (preferred.length > 0) {
196
+ result.push({ type: "divider", id: "__preferred-divider" });
197
+ }
198
+ }
199
+
200
+ const rest =
201
+ preferredSet.size > 0
202
+ ? sortedCountries.filter((c) => !preferredSet.has(c.iso))
203
+ : sortedCountries;
204
+ rest.forEach((c) => result.push(countryToItem(c)));
205
+
206
+ return result;
207
+ });
208
+
209
+ let searchConfig: DropdownMenuSearchConfig = $derived({
210
+ placeholder:
211
+ t?.("checkout.address.country_search_placeholder") || "Search country...",
212
+ strategy: "prefix",
213
+ getContent: (item) => {
214
+ const c = ISO_MAP.get(String(item.id));
215
+ if (!c) return String(item.id);
216
+ const localized = localizedName(c);
217
+ // Search against localized + English + ISO so typing works in either lang.
218
+ return `${localized} ${c.name} ${c.iso}`;
219
+ },
220
+ autoFocus: true,
221
+ noResultsMessage:
222
+ t?.("checkout.address.country_no_results") || "No country found",
223
+ });
224
+
225
+ let triggerText = $derived.by(() => {
226
+ if (selectedCountry) return localizedName(selectedCountry);
227
+ return placeholder ?? "";
228
+ });
229
+ </script>
230
+
231
+ <InputWrap
232
+ {id}
233
+ {label}
234
+ {description}
235
+ {labelAfter}
236
+ {inputBefore}
237
+ {inputAfter}
238
+ {inputBelow}
239
+ {below}
240
+ {required}
241
+ {disabled}
242
+ size={renderSize}
243
+ class={classProp}
244
+ {labelLeft}
245
+ {labelLeftWidth}
246
+ {labelLeftBreakpoint}
247
+ {classLabel}
248
+ {classLabelBox}
249
+ {classInputBox}
250
+ {classInputBoxWrap}
251
+ {classInputBoxWrapInvalid}
252
+ {classDescBox}
253
+ {classDescBoxToggle}
254
+ {classBelowBox}
255
+ {classValidationBox}
256
+ {validation}
257
+ {style}
258
+ >
259
+ <DropdownMenu
260
+ {items}
261
+ bind:isOpen
262
+ position="bottom-span-right"
263
+ search={searchConfig}
264
+ maxHeight="300px"
265
+ closeOnSelect
266
+ class="stuic-field-country"
267
+ classDropdown={twMerge("w-72 max-w-[calc(100vw-1rem)]", classDropdown)}
268
+ >
269
+ {#snippet trigger({ toggle, triggerProps })}
270
+ <!--
271
+ Spread triggerProps (ARIA wiring from DropdownMenu) first, then override
272
+ its `id` with our InputWrap id so the <label for={id}> activates the button
273
+ when clicked. DropdownMenu's internal `aria-labelledby` reference is
274
+ incidentally broken by this swap, but `aria-controls` + `aria-expanded`
275
+ on the trigger and `role="menu"` on the popover keep the menu accessible.
276
+ -->
277
+ <button
278
+ type="button"
279
+ class={twMerge(
280
+ "stuic-field-country-trigger",
281
+ "flex items-center justify-between w-full text-left cursor-pointer",
282
+ "px-3 py-2.5",
283
+ !selectedCountry && "stuic-field-country-placeholder",
284
+ classInput
285
+ )}
286
+ onclick={toggle}
287
+ {disabled}
288
+ {tabindex}
289
+ {...triggerProps}
290
+ {id}
291
+ {...rest}
292
+ >
293
+ <span class="flex-1 min-w-0 truncate">{triggerText || " "}</span>
294
+ <span
295
+ class={twMerge(
296
+ "transition-transform duration-150 shrink-0 ml-2 opacity-60",
297
+ isOpen && "rotate-180"
298
+ )}
299
+ aria-hidden="true"
300
+ >
301
+ {@html iconChevronDown({ size: 16 })}
302
+ </span>
303
+ </button>
304
+ {/snippet}
305
+ </DropdownMenu>
306
+ </InputWrap>
307
+
308
+ <!-- Hidden input for form submission + validation -->
309
+ {#if name}
310
+ <input
311
+ type="hidden"
312
+ {name}
313
+ value={value ?? ""}
314
+ bind:this={hiddenInputEl}
315
+ use:validateAction={() => {
316
+ const customOpts = typeof validate === "object" && validate ? validate : {};
317
+ return {
318
+ enabled: validate !== false,
319
+ ...customOpts,
320
+ setValidationResult,
321
+ };
322
+ }}
323
+ {required}
324
+ {disabled}
325
+ />
326
+ {/if}
@@ -0,0 +1,59 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import type { TranslateFn } from "../../types.js";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { Country } from "./_internal/countries.js";
6
+ import type { InputWrapClassProps } from "./types.js";
7
+ type SnippetWithId = Snippet<[{
8
+ id: string;
9
+ }]>;
10
+ export interface Props extends InputWrapClassProps, Record<string, any> {
11
+ /** Selected country ISO alpha-2 code (e.g. "SK"). Bindable. Empty = unselected. */
12
+ value?: string;
13
+ /** Called whenever the selection changes. */
14
+ onChange?: (iso: string) => void;
15
+ /**
16
+ * Restrict the list to specific countries. Accepts either ISO codes or
17
+ * already-resolved Country objects. Default: all COUNTRIES.
18
+ */
19
+ countryList?: Country[] | string[];
20
+ /** ISO codes to pin at the top of the dropdown, above a divider. */
21
+ preferredCountries?: string[];
22
+ /**
23
+ * Override displayed country names. Keys are ISO alpha-2 codes,
24
+ * values are the localized name. Missing keys fall back to the English
25
+ * name from countries.ts.
26
+ */
27
+ countryNames?: Record<string, string>;
28
+ /** Show country flag emoji in dropdown items. Default: true. */
29
+ flags?: boolean;
30
+ /** Hidden input name (enables form submission + validation). */
31
+ name?: string;
32
+ id?: string;
33
+ tabindex?: number;
34
+ placeholder?: string;
35
+ required?: boolean;
36
+ disabled?: boolean;
37
+ label?: SnippetWithId | THC;
38
+ description?: SnippetWithId | THC;
39
+ class?: string;
40
+ renderSize?: "sm" | "md" | "lg" | string;
41
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
42
+ labelAfter?: SnippetWithId | THC;
43
+ inputBefore?: SnippetWithId | THC;
44
+ inputAfter?: SnippetWithId | THC;
45
+ inputBelow?: SnippetWithId | THC;
46
+ below?: SnippetWithId | THC;
47
+ labelLeft?: boolean;
48
+ labelLeftWidth?: "normal" | "wide";
49
+ labelLeftBreakpoint?: number;
50
+ /** Classes for the underlying trigger <button> element. */
51
+ classInput?: string;
52
+ /** Classes for the dropdown popover. */
53
+ classDropdown?: string;
54
+ style?: string;
55
+ t?: TranslateFn;
56
+ }
57
+ declare const FieldCountry: import("svelte").Component<Props, {}, "value">;
58
+ type FieldCountry = ReturnType<typeof FieldCountry>;
59
+ export default FieldCountry;
@@ -625,4 +625,31 @@
625
625
  padding-top: 0;
626
626
  padding-bottom: 0;
627
627
  }
628
+
629
+ /* ============================================================================
630
+ FIELD COUNTRY
631
+ ============================================================================ */
632
+
633
+ /*
634
+ * DropdownMenu defaults to `display: inline-block` (shrink-to-fit), which
635
+ * makes the trigger button's `w-full` only as wide as its content — so the
636
+ * chevron snaps to the text instead of the right edge. Stretch the wrapper
637
+ * inside the InputWrap's flex container.
638
+ */
639
+ .stuic-field-country.stuic-dropdown-menu {
640
+ display: block;
641
+ flex: 1;
642
+ min-width: 0;
643
+ }
644
+
645
+ /*
646
+ * Reset field-input padding leaking into the popover's search bar
647
+ * (mirrors the phone-prefix-picker fix — the input itself is correctly
648
+ * sized, but its container also gets `.stuic-dropdown-menu-search`
649
+ * padding which stacks on top, making the row look oversized).
650
+ */
651
+ .stuic-field-country .stuic-dropdown-menu-search {
652
+ padding-top: 0;
653
+ padding-bottom: 0;
654
+ }
628
655
  }
@@ -14,5 +14,6 @@ export { default as FieldInputLocalized, type Props as FieldInputLocalizedProps,
14
14
  export { default as FieldKeyValues, type Props as FieldKeyValuesProps, type KeyValueEntry, } from "./FieldKeyValues.svelte";
15
15
  export { default as FieldObject, type Props as FieldObjectProps, } from "./FieldObject.svelte";
16
16
  export { default as FieldPhoneNumber, type Props as FieldPhoneNumberProps, } from "./FieldPhoneNumber.svelte";
17
+ export { default as FieldCountry, type Props as FieldCountryProps, } from "./FieldCountry.svelte";
17
18
  export { validatePhoneNumber } from "./phone-validation.js";
18
- export { type Country } from "./_internal/countries.js";
19
+ export { type Country, COUNTRIES, ISO_MAP } from "./_internal/countries.js";
@@ -14,5 +14,6 @@ export { default as FieldInputLocalized, } from "./FieldInputLocalized.svelte";
14
14
  export { default as FieldKeyValues, } from "./FieldKeyValues.svelte";
15
15
  export { default as FieldObject, } from "./FieldObject.svelte";
16
16
  export { default as FieldPhoneNumber, } from "./FieldPhoneNumber.svelte";
17
+ export { default as FieldCountry, } from "./FieldCountry.svelte";
17
18
  export { validatePhoneNumber } from "./phone-validation.js";
18
- export {} from "./_internal/countries.js";
19
+ export { COUNTRIES, ISO_MAP } from "./_internal/countries.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.83.3",
3
+ "version": "3.85.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",
@@ -88,7 +88,7 @@
88
88
  "@marianmeres/parse-boolean": "^2.1.0",
89
89
  "@marianmeres/ticker": "^1.17.1",
90
90
  "@marianmeres/tree": "^2.3.0",
91
- "libphonenumber-js": "^1.13.1",
91
+ "libphonenumber-js": "^1.13.2",
92
92
  "runed": "^0.23.4",
93
93
  "tailwind-merge": "^3.6.0"
94
94
  }