@djangocfg/ui-core 2.1.146 → 2.1.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @djangocfg/ui-core
2
2
 
3
- Pure React UI library with 60 components built on Radix UI + Tailwind CSS v4.
3
+ Pure React UI library with 61 components built on Radix UI + Tailwind CSS v4.
4
4
 
5
5
  **No Next.js dependencies** — works with Electron, Vite, CRA, and any React environment.
6
6
 
@@ -23,8 +23,8 @@ pnpm add @djangocfg/ui-core
23
23
 
24
24
  ## Components (60)
25
25
 
26
- ### Forms (16)
27
- `Label` `Button` `ButtonLink` `Input` `Checkbox` `RadioGroup` `Select` `Textarea` `Switch` `Slider` `Combobox` `MultiSelect` `InputOTP` `PhoneInput` `Form` `Field`
26
+ ### Forms (17)
27
+ `Label` `Button` `ButtonLink` `Input` `Checkbox` `RadioGroup` `Select` `Textarea` `Switch` `Slider` `Combobox` `MultiSelect` `CountrySelect` `InputOTP` `PhoneInput` `Form` `Field`
28
28
 
29
29
  ### Layout (8)
30
30
  `Card` `Separator` `Skeleton` `AspectRatio` `Sticky` `ScrollArea` `Resizable` `Section`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.146",
3
+ "version": "2.1.148",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -66,7 +66,8 @@
66
66
  "playground": "playground dev"
67
67
  },
68
68
  "peerDependencies": {
69
- "@djangocfg/i18n": "^2.1.146",
69
+ "@djangocfg/i18n": "^2.1.148",
70
+ "consola": "^3.4.2",
70
71
  "lucide-react": "^0.545.0",
71
72
  "moment": "^2.30.1",
72
73
  "react": "^19.0.0",
@@ -74,8 +75,7 @@
74
75
  "react-hook-form": "^7.69.0",
75
76
  "tailwindcss": "^4.1.18",
76
77
  "zod": "^4.0.0",
77
- "zustand": "^5.0.0",
78
- "consola": "^3.4.2"
78
+ "zustand": "^5.0.0"
79
79
  },
80
80
  "dependencies": {
81
81
  "@hookform/resolvers": "^5.2.2",
@@ -111,8 +111,10 @@
111
111
  "class-variance-authority": "^0.7.1",
112
112
  "clsx": "^2.1.1",
113
113
  "cmdk": "1.1.1",
114
+ "countries-list": "^3.2.2",
114
115
  "date-fns": "^4.1.0",
115
116
  "embla-carousel-react": "8.6.0",
117
+ "i18n-iso-countries": "^7.14.0",
116
118
  "input-otp": "1.4.2",
117
119
  "libphonenumber-js": "^1.12.24",
118
120
  "react-day-picker": "9.11.1",
@@ -124,9 +126,9 @@
124
126
  "vaul": "1.1.2"
125
127
  },
126
128
  "devDependencies": {
127
- "@djangocfg/i18n": "^2.1.146",
129
+ "@djangocfg/i18n": "^2.1.148",
128
130
  "@djangocfg/playground": "workspace:*",
129
- "@djangocfg/typescript-config": "^2.1.146",
131
+ "@djangocfg/typescript-config": "^2.1.148",
130
132
  "@types/node": "^24.7.2",
131
133
  "@types/react": "^19.1.0",
132
134
  "@types/react-dom": "^19.1.0",
@@ -0,0 +1,261 @@
1
+ import { useState } from 'react';
2
+ import { defineStory } from '@djangocfg/playground';
3
+ import { CountrySelect, type TCountryCode } from './country-select';
4
+ import { Label } from './label';
5
+
6
+ export default defineStory({
7
+ title: 'Core/CountrySelect',
8
+ component: CountrySelect,
9
+ description: 'Country selector with emoji flags. Supports dropdown and inline variants, single and multiple selection.',
10
+ });
11
+
12
+ // ============================================================================
13
+ // Dropdown Variants
14
+ // ============================================================================
15
+
16
+ export const SingleDropdown = () => {
17
+ const [value, setValue] = useState<string[]>([]);
18
+
19
+ return (
20
+ <div className="max-w-sm space-y-2">
21
+ <Label>Select country</Label>
22
+ <CountrySelect
23
+ value={value}
24
+ onChange={setValue}
25
+ placeholder="Choose a country..."
26
+ />
27
+ {value.length > 0 && (
28
+ <p className="text-sm text-muted-foreground">Selected: {value[0]}</p>
29
+ )}
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export const MultipleDropdown = () => {
35
+ const [value, setValue] = useState<string[]>([]);
36
+
37
+ return (
38
+ <div className="max-w-sm space-y-2">
39
+ <Label>Select countries</Label>
40
+ <CountrySelect
41
+ multiple
42
+ value={value}
43
+ onChange={setValue}
44
+ placeholder="Choose countries..."
45
+ />
46
+ {value.length > 0 && (
47
+ <p className="text-sm text-muted-foreground">Selected: {value.join(', ')}</p>
48
+ )}
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export const WithDefaultValue = () => {
54
+ const [value, setValue] = useState<string[]>(['US', 'GB', 'DE']);
55
+
56
+ return (
57
+ <div className="max-w-sm space-y-2">
58
+ <Label>Preselected countries</Label>
59
+ <CountrySelect
60
+ multiple
61
+ value={value}
62
+ onChange={setValue}
63
+ />
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export const WithNativeNames = () => {
69
+ const [value, setValue] = useState<string[]>([]);
70
+
71
+ return (
72
+ <div className="max-w-sm space-y-2">
73
+ <Label>With native names</Label>
74
+ <CountrySelect
75
+ multiple
76
+ value={value}
77
+ onChange={setValue}
78
+ showNativeName
79
+ />
80
+ </div>
81
+ );
82
+ };
83
+
84
+ // ============================================================================
85
+ // Inline Variants
86
+ // ============================================================================
87
+
88
+ export const InlineSingle = () => {
89
+ const [value, setValue] = useState<string[]>([]);
90
+
91
+ return (
92
+ <div className="max-w-sm space-y-2">
93
+ <Label>Select your country</Label>
94
+ <CountrySelect
95
+ variant="inline"
96
+ value={value}
97
+ onChange={setValue}
98
+ maxHeight={250}
99
+ />
100
+ {value.length > 0 && (
101
+ <p className="text-sm text-muted-foreground">Selected: {value[0]}</p>
102
+ )}
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export const InlineMultiple = () => {
108
+ const [value, setValue] = useState<string[]>([]);
109
+
110
+ return (
111
+ <div className="max-w-sm space-y-2">
112
+ <Label>Select target markets</Label>
113
+ <CountrySelect
114
+ variant="inline"
115
+ multiple
116
+ value={value}
117
+ onChange={setValue}
118
+ maxHeight={300}
119
+ />
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export const InlineWithNativeNames = () => {
125
+ const [value, setValue] = useState<string[]>(['KR', 'JP']);
126
+
127
+ return (
128
+ <div className="max-w-sm space-y-2">
129
+ <Label>Countries with native names</Label>
130
+ <CountrySelect
131
+ variant="inline"
132
+ multiple
133
+ value={value}
134
+ onChange={setValue}
135
+ showNativeName
136
+ maxHeight={300}
137
+ />
138
+ </div>
139
+ );
140
+ };
141
+
142
+ export const InlineNoSearch = () => {
143
+ const [value, setValue] = useState<string[]>([]);
144
+
145
+ return (
146
+ <div className="max-w-sm space-y-2">
147
+ <Label>Without search</Label>
148
+ <CountrySelect
149
+ variant="inline"
150
+ multiple
151
+ value={value}
152
+ onChange={setValue}
153
+ showSearch={false}
154
+ maxHeight={200}
155
+ />
156
+ </div>
157
+ );
158
+ };
159
+
160
+ // ============================================================================
161
+ // Filtered Countries
162
+ // ============================================================================
163
+
164
+ const CIS_COUNTRIES: TCountryCode[] = ['RU', 'KZ', 'UZ', 'KG', 'TJ', 'TM', 'AZ', 'AM', 'GE', 'BY', 'MD', 'UA'];
165
+ const ASIAN_AUTO_MARKETS: TCountryCode[] = ['KR', 'JP', 'CN', 'TH', 'MY', 'ID', 'VN', 'PH'];
166
+
167
+ export const FilteredCISCountries = () => {
168
+ const [value, setValue] = useState<string[]>([]);
169
+
170
+ return (
171
+ <div className="max-w-sm space-y-2">
172
+ <Label>CIS Countries only</Label>
173
+ <CountrySelect
174
+ variant="inline"
175
+ multiple
176
+ value={value}
177
+ onChange={setValue}
178
+ allowedCountries={CIS_COUNTRIES}
179
+ maxHeight={300}
180
+ />
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export const FilteredAsianMarkets = () => {
186
+ const [value, setValue] = useState<string[]>([]);
187
+
188
+ return (
189
+ <div className="max-w-sm space-y-2">
190
+ <Label>Asian Auto Markets</Label>
191
+ <CountrySelect
192
+ multiple
193
+ value={value}
194
+ onChange={setValue}
195
+ allowedCountries={ASIAN_AUTO_MARKETS}
196
+ showNativeName
197
+ />
198
+ </div>
199
+ );
200
+ };
201
+
202
+ export const ExcludeSanctioned = () => {
203
+ const [value, setValue] = useState<string[]>([]);
204
+ const excluded: TCountryCode[] = ['KP', 'IR', 'SY', 'CU'];
205
+
206
+ return (
207
+ <div className="max-w-sm space-y-2">
208
+ <Label>All except sanctioned</Label>
209
+ <CountrySelect
210
+ multiple
211
+ value={value}
212
+ onChange={setValue}
213
+ excludedCountries={excluded}
214
+ />
215
+ </div>
216
+ );
217
+ };
218
+
219
+ // ============================================================================
220
+ // States
221
+ // ============================================================================
222
+
223
+ export const Disabled = () => (
224
+ <div className="max-w-sm space-y-4">
225
+ <div className="space-y-2">
226
+ <Label>Disabled dropdown</Label>
227
+ <CountrySelect
228
+ value={['US']}
229
+ onChange={() => {}}
230
+ disabled
231
+ />
232
+ </div>
233
+ <div className="space-y-2">
234
+ <Label>Disabled inline</Label>
235
+ <CountrySelect
236
+ variant="inline"
237
+ multiple
238
+ value={['US', 'GB']}
239
+ onChange={() => {}}
240
+ disabled
241
+ maxHeight={150}
242
+ />
243
+ </div>
244
+ </div>
245
+ );
246
+
247
+ export const MaxDisplayBadges = () => {
248
+ const [value, setValue] = useState<string[]>(['US', 'GB', 'DE', 'FR', 'IT', 'ES']);
249
+
250
+ return (
251
+ <div className="max-w-sm space-y-2">
252
+ <Label>Max 2 badges displayed</Label>
253
+ <CountrySelect
254
+ multiple
255
+ value={value}
256
+ onChange={setValue}
257
+ maxDisplay={2}
258
+ />
259
+ </div>
260
+ );
261
+ };
@@ -0,0 +1,405 @@
1
+ "use client"
2
+
3
+ import { Check, ChevronsUpDown, Search, X } from 'lucide-react';
4
+ import * as React from 'react';
5
+ import { countries, getEmojiFlag, type TCountryCode } from 'countries-list';
6
+
7
+ import { cn } from '../lib/utils';
8
+ import { Badge } from './badge';
9
+ import { Button } from './button';
10
+ import { Checkbox } from './checkbox';
11
+ import {
12
+ Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList
13
+ } from './command';
14
+ import { Input } from './input';
15
+ import { Popover, PopoverContent, PopoverTrigger } from './popover';
16
+ import { ScrollArea } from './scroll-area';
17
+
18
+ export interface CountryOption {
19
+ code: TCountryCode;
20
+ name: string;
21
+ native: string;
22
+ emoji: string;
23
+ }
24
+
25
+ export type CountrySelectVariant = 'dropdown' | 'inline';
26
+
27
+ export interface CountrySelectProps {
28
+ /** Selected country codes (ISO 3166-1 alpha-2) */
29
+ value?: string[];
30
+ /** Callback when selection changes */
31
+ onChange?: (value: string[]) => void;
32
+ /** Allow multiple selection */
33
+ multiple?: boolean;
34
+ /** Display variant: dropdown (popover) or inline (scrollable list) */
35
+ variant?: CountrySelectVariant;
36
+ /** Placeholder text (default: "Select country...") */
37
+ placeholder?: string;
38
+ /** Search placeholder text (default: "Search...") */
39
+ searchPlaceholder?: string;
40
+ /** Empty results text (default: "No countries found") */
41
+ emptyText?: string;
42
+ /** Additional CSS class */
43
+ className?: string;
44
+ /** Disable the component */
45
+ disabled?: boolean;
46
+ /** Max badges to display (for multiple dropdown mode) */
47
+ maxDisplay?: number;
48
+ /** Custom country name resolver (for i18n) */
49
+ getCountryName?: (code: TCountryCode) => string;
50
+ /** Show native name alongside translated name */
51
+ showNativeName?: boolean;
52
+ /** Filter to specific country codes */
53
+ allowedCountries?: TCountryCode[];
54
+ /** Exclude specific country codes */
55
+ excludedCountries?: TCountryCode[];
56
+ /** Max height for inline variant */
57
+ maxHeight?: number;
58
+ /** Show search input */
59
+ showSearch?: boolean;
60
+ /** Custom label for selected count (receives count as param). Example: (count) => `${count} selected` */
61
+ selectedCountLabel?: (count: number) => string;
62
+ /** Custom label for "more items" badge (receives count as param). Example: (count) => `+${count} more` */
63
+ moreItemsLabel?: (count: number) => string;
64
+ }
65
+
66
+ /**
67
+ * Country Select component with emoji flags
68
+ *
69
+ * Supports:
70
+ * - Single and multiple selection
71
+ * - Dropdown (popover) and inline (scrollable list) variants
72
+ * - Custom country name translations via getCountryName prop
73
+ * - Country filtering via allowedCountries/excludedCountries
74
+ *
75
+ * Uses ISO 3166-1 alpha-2 country codes.
76
+ *
77
+ * @example Single dropdown
78
+ * ```tsx
79
+ * <CountrySelect
80
+ * value={country ? [country] : []}
81
+ * onChange={(codes) => setCountry(codes[0])}
82
+ * />
83
+ * ```
84
+ *
85
+ * @example Multiple dropdown
86
+ * ```tsx
87
+ * <CountrySelect
88
+ * multiple
89
+ * value={countries}
90
+ * onChange={setCountries}
91
+ * />
92
+ * ```
93
+ *
94
+ * @example Inline list with checkboxes
95
+ * ```tsx
96
+ * <CountrySelect
97
+ * variant="inline"
98
+ * multiple
99
+ * value={countries}
100
+ * onChange={setCountries}
101
+ * maxHeight={300}
102
+ * />
103
+ * ```
104
+ *
105
+ * @example With i18n translations
106
+ * ```tsx
107
+ * <CountrySelect
108
+ * value={value}
109
+ * onChange={onChange}
110
+ * getCountryName={(code) => i18nCountries.getName(code, locale)}
111
+ * />
112
+ * ```
113
+ */
114
+ export function CountrySelect({
115
+ value = [],
116
+ onChange,
117
+ multiple = false,
118
+ variant = 'dropdown',
119
+ placeholder,
120
+ searchPlaceholder,
121
+ emptyText,
122
+ className,
123
+ disabled = false,
124
+ maxDisplay = 3,
125
+ getCountryName,
126
+ showNativeName = false,
127
+ allowedCountries,
128
+ excludedCountries,
129
+ maxHeight = 300,
130
+ showSearch = true,
131
+ selectedCountLabel = (count: number) => `${count} selected`,
132
+ moreItemsLabel = (count: number) => `+${count} more`,
133
+ }: CountrySelectProps) {
134
+ const [open, setOpen] = React.useState(false)
135
+ const [search, setSearch] = React.useState("")
136
+
137
+ // Resolve defaults
138
+ const resolvedPlaceholder = placeholder ?? 'Select country...'
139
+ const resolvedSearchPlaceholder = searchPlaceholder ?? 'Search...'
140
+ const resolvedEmptyText = emptyText ?? 'No countries found'
141
+
142
+ // Build country options
143
+ const allCountries = React.useMemo<CountryOption[]>(() => {
144
+ let codes = Object.keys(countries) as TCountryCode[];
145
+
146
+ // Apply filters
147
+ if (allowedCountries?.length) {
148
+ codes = codes.filter(code => allowedCountries.includes(code));
149
+ }
150
+ if (excludedCountries?.length) {
151
+ codes = codes.filter(code => !excludedCountries.includes(code));
152
+ }
153
+
154
+ return codes
155
+ .map((code) => ({
156
+ code,
157
+ name: getCountryName?.(code) ?? countries[code].name,
158
+ native: countries[code].native,
159
+ emoji: getEmojiFlag(code),
160
+ }))
161
+ .sort((a, b) => a.name.localeCompare(b.name));
162
+ }, [getCountryName, allowedCountries, excludedCountries]);
163
+
164
+ const selectedCountries = React.useMemo(
165
+ () => allCountries.filter((c) => value.includes(c.code)),
166
+ [allCountries, value]
167
+ )
168
+
169
+ const filteredCountries = React.useMemo(() => {
170
+ if (!search) return allCountries
171
+ const searchLower = search.toLowerCase()
172
+ return allCountries.filter(
173
+ (c) =>
174
+ c.name.toLowerCase().includes(searchLower) ||
175
+ c.native.toLowerCase().includes(searchLower) ||
176
+ c.code.toLowerCase().includes(searchLower)
177
+ )
178
+ }, [allCountries, search])
179
+
180
+ const handleSelect = React.useCallback((code: string) => {
181
+ if (multiple) {
182
+ const newValue = value.includes(code)
183
+ ? value.filter((v) => v !== code)
184
+ : [...value, code]
185
+ onChange?.(newValue)
186
+ } else {
187
+ onChange?.([code])
188
+ if (variant === 'dropdown') {
189
+ setOpen(false)
190
+ }
191
+ }
192
+ }, [multiple, value, onChange, variant])
193
+
194
+ const handleRemove = React.useCallback((code: string, e: React.MouseEvent) => {
195
+ e.stopPropagation()
196
+ onChange?.(value.filter((v) => v !== code))
197
+ }, [value, onChange])
198
+
199
+ // Inline variant
200
+ if (variant === 'inline') {
201
+ return (
202
+ <div className={cn("space-y-3", className)}>
203
+ {/* Search input */}
204
+ {showSearch && (
205
+ <div className="relative">
206
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
207
+ <Input
208
+ type="text"
209
+ placeholder={resolvedSearchPlaceholder}
210
+ value={search}
211
+ onChange={(e) => setSearch(e.target.value)}
212
+ className="pl-9"
213
+ disabled={disabled}
214
+ />
215
+ </div>
216
+ )}
217
+
218
+ {/* Selected count */}
219
+ {multiple && selectedCountries.length > 0 && (
220
+ <p className="text-sm text-muted-foreground">
221
+ {selectedCountLabel(selectedCountries.length)}
222
+ </p>
223
+ )}
224
+
225
+ {/* Country list */}
226
+ <ScrollArea style={{ height: maxHeight }} className="rounded-md border">
227
+ <div className="p-1">
228
+ {filteredCountries.length === 0 ? (
229
+ <p className="text-sm text-muted-foreground text-center py-4">
230
+ {resolvedEmptyText}
231
+ </p>
232
+ ) : (
233
+ filteredCountries.map((country) => {
234
+ const isSelected = value.includes(country.code);
235
+ return (
236
+ <label
237
+ key={country.code}
238
+ className={cn(
239
+ 'flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer',
240
+ 'hover:bg-accent/50 transition-colors',
241
+ isSelected && 'bg-primary/5',
242
+ disabled && 'opacity-50 cursor-not-allowed',
243
+ )}
244
+ >
245
+ {multiple ? (
246
+ <Checkbox
247
+ checked={isSelected}
248
+ onCheckedChange={() => !disabled && handleSelect(country.code)}
249
+ disabled={disabled}
250
+ />
251
+ ) : (
252
+ <div className={cn(
253
+ "h-4 w-4 rounded-full border-2 flex items-center justify-center",
254
+ isSelected ? "border-primary bg-primary" : "border-muted-foreground"
255
+ )}>
256
+ {isSelected && <div className="h-2 w-2 rounded-full bg-primary-foreground" />}
257
+ </div>
258
+ )}
259
+ <span className="text-lg">{country.emoji}</span>
260
+ <div className="flex flex-col flex-1 min-w-0">
261
+ <span className={cn('text-sm', isSelected && 'font-medium')}>
262
+ {country.name}
263
+ </span>
264
+ {showNativeName && country.native !== country.name && (
265
+ <span className="text-xs text-muted-foreground">
266
+ {country.native}
267
+ </span>
268
+ )}
269
+ </div>
270
+ </label>
271
+ );
272
+ })
273
+ )}
274
+ </div>
275
+ </ScrollArea>
276
+ </div>
277
+ );
278
+ }
279
+
280
+ // Dropdown variant
281
+ const displayValue = React.useMemo(() => {
282
+ if (selectedCountries.length === 0) {
283
+ return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
284
+ }
285
+
286
+ if (!multiple && selectedCountries.length === 1) {
287
+ const country = selectedCountries[0];
288
+ return (
289
+ <div className="flex items-center gap-2">
290
+ <span>{country.emoji}</span>
291
+ <span>{country.name}</span>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ const displayed = selectedCountries.slice(0, maxDisplay)
297
+ const remaining = selectedCountries.length - maxDisplay
298
+
299
+ return (
300
+ <div className="flex flex-wrap gap-1">
301
+ {displayed.map((country) => (
302
+ <Badge
303
+ key={country.code}
304
+ variant="secondary"
305
+ className="mr-1 text-xs"
306
+ >
307
+ <span className="mr-1">{country.emoji}</span>
308
+ {country.name}
309
+ <button
310
+ className="ml-1 rounded-full hover:bg-muted-foreground/20"
311
+ onClick={(e) => handleRemove(country.code, e)}
312
+ disabled={disabled}
313
+ aria-label={`Remove ${country.name}`}
314
+ >
315
+ <X className="h-3 w-3" />
316
+ </button>
317
+ </Badge>
318
+ ))}
319
+ {remaining > 0 && (
320
+ <Badge variant="outline" className="text-xs">
321
+ {moreItemsLabel(remaining)}
322
+ </Badge>
323
+ )}
324
+ </div>
325
+ )
326
+ }, [selectedCountries, maxDisplay, resolvedPlaceholder, disabled, multiple, handleRemove, moreItemsLabel])
327
+
328
+ return (
329
+ <Popover
330
+ open={open}
331
+ onOpenChange={(isOpen) => {
332
+ setOpen(isOpen)
333
+ if (!isOpen) {
334
+ setSearch("")
335
+ }
336
+ }}
337
+ >
338
+ <PopoverTrigger asChild>
339
+ <Button
340
+ variant="outline"
341
+ role="combobox"
342
+ aria-expanded={open}
343
+ className={cn(
344
+ "w-full justify-between min-h-10 h-auto py-2",
345
+ className
346
+ )}
347
+ disabled={disabled}
348
+ >
349
+ <div className="flex-1 text-left overflow-hidden">
350
+ {displayValue}
351
+ </div>
352
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
353
+ </Button>
354
+ </PopoverTrigger>
355
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
356
+ <Command shouldFilter={false} className="flex flex-col">
357
+ <CommandInput
358
+ placeholder={resolvedSearchPlaceholder}
359
+ className="shrink-0"
360
+ value={search}
361
+ onValueChange={setSearch}
362
+ />
363
+ <CommandList className="max-h-[300px]">
364
+ {filteredCountries.length === 0 ? (
365
+ <CommandEmpty>{resolvedEmptyText}</CommandEmpty>
366
+ ) : (
367
+ <CommandGroup>
368
+ {filteredCountries.map((country) => {
369
+ const isSelected = value.includes(country.code)
370
+ return (
371
+ <CommandItem
372
+ key={country.code}
373
+ value={country.code}
374
+ onSelect={() => handleSelect(country.code)}
375
+ >
376
+ <Check
377
+ className={cn(
378
+ "mr-2 h-4 w-4 shrink-0",
379
+ isSelected ? "opacity-100" : "opacity-0"
380
+ )}
381
+ />
382
+ <span className="mr-2 text-lg">{country.emoji}</span>
383
+ <div className="flex flex-col flex-1 min-w-0">
384
+ <span className="truncate">{country.name}</span>
385
+ {showNativeName && country.native !== country.name && (
386
+ <span className="text-xs text-muted-foreground truncate">
387
+ {country.native}
388
+ </span>
389
+ )}
390
+ </div>
391
+ </CommandItem>
392
+ )
393
+ })}
394
+ </CommandGroup>
395
+ )}
396
+ </CommandList>
397
+ </Command>
398
+ </PopoverContent>
399
+ </Popover>
400
+ )
401
+ }
402
+
403
+ // Re-export types for convenience
404
+ export type { TCountryCode } from 'countries-list';
405
+ export { getEmojiFlag } from 'countries-list';
@@ -21,6 +21,8 @@ export { MultiSelect } from './multi-select';
21
21
  export type { MultiSelectOption, MultiSelectProps } from './multi-select';
22
22
  export { MultiSelectPro } from './multi-select-pro';
23
23
  export type { MultiSelectProOption, MultiSelectProGroup, MultiSelectProProps, MultiSelectProRef, AnimationConfig, ResponsiveConfig } from './multi-select-pro';
24
+ export { CountrySelect, getEmojiFlag } from './country-select';
25
+ export type { CountrySelectProps, CountrySelectVariant, CountryOption, TCountryCode } from './country-select';
24
26
  export { MultiSelectProAsync } from './multi-select-pro/async';
25
27
  export type { MultiSelectProAsyncProps } from './multi-select-pro/async';
26
28
  export { Switch } from './switch';