@marianmeres/stuic 3.84.0 → 3.86.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
  /**
@@ -42,12 +50,13 @@
42
50
 
43
51
  /**
44
52
  * Override the country field with a custom selector.
45
- * 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.
46
55
  */
47
56
  countryField?: Snippet<
48
57
  [
49
58
  {
50
- /** Current country value */
59
+ /** Current country value (ISO alpha-2 code) */
51
60
  value: string;
52
61
  /** Called when country changes */
53
62
  onchange: (value: string) => void;
@@ -61,9 +70,29 @@
61
70
  ]
62
71
  >;
63
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
+
64
90
  /** Extra props forwarded to the internal FieldPhoneNumber component. */
65
91
  phoneFieldProps?: Partial<FieldPhoneNumberProps>;
66
92
 
93
+ /** Extra props forwarded to the internal FieldCountry component. */
94
+ countryFieldProps?: Partial<FieldCountryProps>;
95
+
67
96
  t?: TranslateFn;
68
97
  unstyled?: boolean;
69
98
  class?: string;
@@ -77,6 +106,7 @@
77
106
  import { createEmptyAddress } from "./_internal/checkout-utils.js";
78
107
  import FieldInput from "../Input/FieldInput.svelte";
79
108
  import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
109
+ import FieldCountry from "../Input/FieldCountry.svelte";
80
110
  import { validatePhoneNumber } from "../Input/phone-validation.js";
81
111
 
82
112
  const DEFAULT_REQUIRED = ["name", "street", "city", "postal_code", "country"];
@@ -88,7 +118,11 @@
88
118
  fields,
89
119
  requiredFields = DEFAULT_REQUIRED,
90
120
  countryField,
121
+ countryList,
122
+ preferredCountries,
123
+ countryNames,
91
124
  phoneFieldProps,
125
+ countryFieldProps,
92
126
  t: tProp,
93
127
  unstyled = false,
94
128
  class: classProp,
@@ -235,7 +269,7 @@
235
269
  })}
236
270
  {:else}
237
271
  <!-- svelte-ignore binding_property_non_reactive -->
238
- <FieldInput
272
+ <FieldCountry
239
273
  bind:value={address.country}
240
274
  label={t("checkout.address.country_label")}
241
275
  placeholder={t("checkout.address.country_placeholder")}
@@ -243,11 +277,16 @@
243
277
  name="{label}-country"
244
278
  id="{label}-country"
245
279
  labelLeftBreakpoint={0}
280
+ {countryList}
281
+ {preferredCountries}
282
+ {countryNames}
283
+ t={tProp}
246
284
  validate={{
247
285
  customValidator(val) {
248
286
  return fieldError("country") || "";
249
287
  },
250
288
  }}
289
+ {...countryFieldProps}
251
290
  />
252
291
  {/if}
253
292
  {/if}
@@ -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:
@@ -32,11 +40,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
32
40
  requiredFields?: string[];
33
41
  /**
34
42
  * Override the country field with a custom selector.
35
- * 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.
36
45
  */
37
46
  countryField?: Snippet<[
38
47
  {
39
- /** Current country value */
48
+ /** Current country value (ISO alpha-2 code) */
40
49
  value: string;
41
50
  /** Called when country changes */
42
51
  onchange: (value: string) => void;
@@ -48,8 +57,24 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
48
57
  id: string;
49
58
  }
50
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>;
51
74
  /** Extra props forwarded to the internal FieldPhoneNumber component. */
52
75
  phoneFieldProps?: Partial<FieldPhoneNumberProps>;
76
+ /** Extra props forwarded to the internal FieldCountry component. */
77
+ countryFieldProps?: Partial<FieldCountryProps>;
53
78
  t?: TranslateFn;
54
79
  unstyled?: boolean;
55
80
  class?: string;
@@ -70,6 +70,8 @@ const DEFAULTS = {
70
70
  "checkout.address.postal_code_placeholder": "",
71
71
  "checkout.address.country_label": "Country",
72
72
  "checkout.address.country_placeholder": "",
73
+ "checkout.address.country_search_placeholder": "Search country...",
74
+ "checkout.address.country_no_results": "No country found",
73
75
  "checkout.address.phone_label": "Phone",
74
76
  "checkout.address.phone_placeholder": "",
75
77
  "checkout.address.required_marker": "*",
@@ -0,0 +1,329 @@
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 triggerEl: HTMLButtonElement | undefined = $state();
135
+ let hiddenInputEl: HTMLInputElement | undefined = $state();
136
+ let validation: ValidationResult | undefined = $state();
137
+ const setValidationResult = (res: ValidationResult) => (validation = res);
138
+
139
+ function localizedName(c: Country): string {
140
+ return countryNames?.[c.iso] ?? c.name;
141
+ }
142
+
143
+ // Resolve the working country list (accept ISO codes or Country objects).
144
+ let resolvedCountries: Country[] = $derived.by(() => {
145
+ if (!countryListProp) return COUNTRIES;
146
+ if (countryListProp.length === 0) return [];
147
+ if (typeof countryListProp[0] === "string") {
148
+ const set = new Set(
149
+ (countryListProp as string[]).map((c) => c.toUpperCase())
150
+ );
151
+ return COUNTRIES.filter((c) => set.has(c.iso));
152
+ }
153
+ return countryListProp as Country[];
154
+ });
155
+
156
+ // Sort resolved list alphabetically by displayed name (locale-aware).
157
+ let sortedCountries: Country[] = $derived(
158
+ [...resolvedCountries].sort((a, b) =>
159
+ localizedName(a).localeCompare(localizedName(b))
160
+ )
161
+ );
162
+
163
+ let selectedCountry: Country | undefined = $derived(
164
+ value ? ISO_MAP.get(value.toUpperCase()) : undefined
165
+ );
166
+
167
+ function countryToItem(c: Country): DropdownMenuActionItem {
168
+ const name = localizedName(c);
169
+ const prefix = flags ? `${c.flag} ` : "";
170
+ return {
171
+ type: "action",
172
+ id: c.iso,
173
+ label: `${prefix}${name}`,
174
+ onSelect: () => {
175
+ value = c.iso;
176
+ onChange?.(c.iso);
177
+ // Trigger change on hidden input so validation runs.
178
+ hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
179
+ },
180
+ };
181
+ }
182
+
183
+ let items: DropdownMenuItem[] = $derived.by(() => {
184
+ const result: DropdownMenuItem[] = [];
185
+ const preferredSet = new Set(
186
+ preferredCountries?.map((c) => c.toUpperCase()) ?? []
187
+ );
188
+
189
+ if (preferredSet.size > 0) {
190
+ // Preserve the order given in `preferredCountries`.
191
+ const order = preferredCountries!.map((c) => c.toUpperCase());
192
+ const preferred = order
193
+ .map((iso) => resolvedCountries.find((c) => c.iso === iso))
194
+ .filter((c): c is Country => !!c);
195
+ preferred.forEach((c) => result.push(countryToItem(c)));
196
+ if (preferred.length > 0) {
197
+ result.push({ type: "divider", id: "__preferred-divider" });
198
+ }
199
+ }
200
+
201
+ const rest =
202
+ preferredSet.size > 0
203
+ ? sortedCountries.filter((c) => !preferredSet.has(c.iso))
204
+ : sortedCountries;
205
+ rest.forEach((c) => result.push(countryToItem(c)));
206
+
207
+ return result;
208
+ });
209
+
210
+ let searchConfig: DropdownMenuSearchConfig = $derived({
211
+ placeholder:
212
+ t?.("checkout.address.country_search_placeholder") || "Search country...",
213
+ strategy: "prefix",
214
+ getContent: (item) => {
215
+ const c = ISO_MAP.get(String(item.id));
216
+ if (!c) return String(item.id);
217
+ const localized = localizedName(c);
218
+ // Search against localized + English + ISO so typing works in either lang.
219
+ return `${localized} ${c.name} ${c.iso}`;
220
+ },
221
+ autoFocus: true,
222
+ noResultsMessage:
223
+ t?.("checkout.address.country_no_results") || "No country found",
224
+ });
225
+
226
+ let triggerText = $derived.by(() => {
227
+ if (selectedCountry) return localizedName(selectedCountry);
228
+ return placeholder ?? "";
229
+ });
230
+ </script>
231
+
232
+ <InputWrap
233
+ {id}
234
+ {label}
235
+ {description}
236
+ {labelAfter}
237
+ {inputBefore}
238
+ {inputAfter}
239
+ {inputBelow}
240
+ {below}
241
+ {required}
242
+ {disabled}
243
+ size={renderSize}
244
+ class={classProp}
245
+ {labelLeft}
246
+ {labelLeftWidth}
247
+ {labelLeftBreakpoint}
248
+ {classLabel}
249
+ {classLabelBox}
250
+ {classInputBox}
251
+ {classInputBoxWrap}
252
+ {classInputBoxWrapInvalid}
253
+ {classDescBox}
254
+ {classDescBoxToggle}
255
+ {classBelowBox}
256
+ {classValidationBox}
257
+ {validation}
258
+ {style}
259
+ >
260
+ <DropdownMenu
261
+ {items}
262
+ bind:isOpen
263
+ bind:triggerEl
264
+ position="bottom-span-right"
265
+ search={searchConfig}
266
+ maxHeight="300px"
267
+ closeOnSelect
268
+ class="stuic-field-country"
269
+ classDropdown={twMerge("stuic-field-country-dropdown", classDropdown)}
270
+ >
271
+ {#snippet trigger({ toggle, triggerProps })}
272
+ <!--
273
+ Spread triggerProps (ARIA wiring from DropdownMenu) first, then override
274
+ its `id` with our InputWrap id so the <label for={id}> activates the button
275
+ when clicked. DropdownMenu's internal `aria-labelledby` reference is
276
+ incidentally broken by this swap, but `aria-controls` + `aria-expanded`
277
+ on the trigger and `role="menu"` on the popover keep the menu accessible.
278
+ -->
279
+ <button
280
+ bind:this={triggerEl}
281
+ type="button"
282
+ class={twMerge(
283
+ "stuic-field-country-trigger",
284
+ "flex items-center justify-between w-full text-left cursor-pointer",
285
+ "px-3 py-2.5",
286
+ !selectedCountry && "stuic-field-country-placeholder",
287
+ classInput
288
+ )}
289
+ onclick={toggle}
290
+ {disabled}
291
+ {tabindex}
292
+ {...triggerProps}
293
+ {id}
294
+ {...rest}
295
+ >
296
+ <span class="flex-1 min-w-0 truncate">{triggerText || " "}</span>
297
+ <span
298
+ class={twMerge(
299
+ "transition-transform duration-150 shrink-0 ml-2 opacity-60",
300
+ isOpen && "rotate-180"
301
+ )}
302
+ aria-hidden="true"
303
+ >
304
+ {@html iconChevronDown({ size: 16 })}
305
+ </span>
306
+ </button>
307
+ {/snippet}
308
+ </DropdownMenu>
309
+ </InputWrap>
310
+
311
+ <!-- Hidden input for form submission + validation -->
312
+ {#if name}
313
+ <input
314
+ type="hidden"
315
+ {name}
316
+ value={value ?? ""}
317
+ bind:this={hiddenInputEl}
318
+ use:validateAction={() => {
319
+ const customOpts = typeof validate === "object" && validate ? validate : {};
320
+ return {
321
+ enabled: validate !== false,
322
+ ...customOpts,
323
+ setValidationResult,
324
+ };
325
+ }}
326
+ {required}
327
+ {disabled}
328
+ />
329
+ {/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;
@@ -84,6 +84,7 @@ export const COUNTRIES = [
84
84
  { iso: "GP", name: "Guadeloupe", dialCode: "590", flag: "🇬🇵" },
85
85
  { iso: "GU", name: "Guam", dialCode: "1671", flag: "🇬🇺" },
86
86
  { iso: "GT", name: "Guatemala", dialCode: "502", flag: "🇬🇹" },
87
+ { iso: "GG", name: "Guernsey", dialCode: "44", flag: "🇬🇬" },
87
88
  { iso: "GN", name: "Guinea", dialCode: "224", flag: "🇬🇳" },
88
89
  { iso: "GW", name: "Guinea-Bissau", dialCode: "245", flag: "🇬🇼" },
89
90
  { iso: "GY", name: "Guyana", dialCode: "592", flag: "🇬🇾" },
@@ -97,11 +98,13 @@ export const COUNTRIES = [
97
98
  { iso: "IR", name: "Iran", dialCode: "98", flag: "🇮🇷" },
98
99
  { iso: "IQ", name: "Iraq", dialCode: "964", flag: "🇮🇶" },
99
100
  { iso: "IE", name: "Ireland", dialCode: "353", flag: "🇮🇪" },
101
+ { iso: "IM", name: "Isle of Man", dialCode: "44", flag: "🇮🇲" },
100
102
  { iso: "IL", name: "Israel", dialCode: "972", flag: "🇮🇱" },
101
103
  { iso: "IT", name: "Italy", dialCode: "39", flag: "🇮🇹" },
102
104
  { iso: "CI", name: "Ivory Coast", dialCode: "225", flag: "🇨🇮" },
103
105
  { iso: "JM", name: "Jamaica", dialCode: "1876", flag: "🇯🇲" },
104
106
  { iso: "JP", name: "Japan", dialCode: "81", flag: "🇯🇵" },
107
+ { iso: "JE", name: "Jersey", dialCode: "44", flag: "🇯🇪" },
105
108
  { iso: "JO", name: "Jordan", dialCode: "962", flag: "🇯🇴" },
106
109
  { iso: "KZ", name: "Kazakhstan", dialCode: "7", flag: "🇰🇿" },
107
110
  { iso: "KE", name: "Kenya", dialCode: "254", flag: "🇰🇪" },
@@ -625,4 +625,52 @@
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
+ * Suppress the browser's default focus ring on the trigger — InputWrap
647
+ * already shows the ring via `.input-wrap:focus-within`.
648
+ */
649
+ .stuic-field-country-trigger:focus,
650
+ .stuic-field-country-trigger:focus-visible {
651
+ outline: none;
652
+ box-shadow: none;
653
+ }
654
+
655
+ /*
656
+ * Size the popover to the trigger via CSS Anchor Positioning's
657
+ * `anchor-size()`. In browsers without anchor positioning, DropdownMenu
658
+ * already switches to a centered-modal fallback, so this rule simply
659
+ * doesn't apply there. `min-width` keeps very narrow triggers usable.
660
+ */
661
+ .stuic-field-country-dropdown {
662
+ width: anchor-size(width);
663
+ min-width: 12rem;
664
+ }
665
+
666
+ /*
667
+ * Reset field-input padding leaking into the popover's search bar
668
+ * (mirrors the phone-prefix-picker fix — the input itself is correctly
669
+ * sized, but its container also gets `.stuic-dropdown-menu-search`
670
+ * padding which stacks on top, making the row look oversized).
671
+ */
672
+ .stuic-field-country .stuic-dropdown-menu-search {
673
+ padding-top: 0;
674
+ padding-bottom: 0;
675
+ }
628
676
  }
@@ -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.84.0",
3
+ "version": "3.86.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",