@djangocfg/ui-core 2.1.109 → 2.1.111
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/package.json +4 -2
- package/src/components/calendar/date-picker.tsx +11 -4
- package/src/components/carousel.tsx +7 -2
- package/src/components/combobox.tsx +13 -6
- package/src/components/copy.tsx +6 -2
- package/src/components/index.ts +1 -0
- package/src/components/multi-select-pro/async.tsx +27 -14
- package/src/components/multi-select-pro/index.tsx +22 -9
- package/src/components/multi-select.tsx +16 -8
- package/src/components/phone-input.tsx +11 -3
- package/src/components/preloader.tsx +5 -2
- package/src/components/spinner.tsx +5 -1
- package/src/components/tabs.tsx +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.111",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"check": "tsc --noEmit"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
+
"@djangocfg/i18n": "^2.1.111",
|
|
68
69
|
"lucide-react": "^0.545.0",
|
|
69
70
|
"moment": "^2.30.1",
|
|
70
71
|
"react": "^19.0.0",
|
|
@@ -122,7 +123,8 @@
|
|
|
122
123
|
"vaul": "1.1.2"
|
|
123
124
|
},
|
|
124
125
|
"devDependencies": {
|
|
125
|
-
"@djangocfg/
|
|
126
|
+
"@djangocfg/i18n": "^2.1.111",
|
|
127
|
+
"@djangocfg/typescript-config": "^2.1.111",
|
|
126
128
|
"@types/node": "^24.7.2",
|
|
127
129
|
"@types/react": "^19.1.0",
|
|
128
130
|
"@types/react-dom": "^19.1.0",
|
|
@@ -4,6 +4,7 @@ import { format } from 'date-fns';
|
|
|
4
4
|
import { CalendarIcon } from 'lucide-react';
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
7
8
|
import { cn } from '../../lib/utils';
|
|
8
9
|
import { Button } from '../button';
|
|
9
10
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
@@ -43,7 +44,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
|
43
44
|
{
|
|
44
45
|
value,
|
|
45
46
|
onChange,
|
|
46
|
-
placeholder
|
|
47
|
+
placeholder,
|
|
47
48
|
dateFormat = 'PPP',
|
|
48
49
|
disabled = false,
|
|
49
50
|
fromDate,
|
|
@@ -54,8 +55,11 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
|
54
55
|
},
|
|
55
56
|
ref
|
|
56
57
|
) => {
|
|
58
|
+
const t = useTypedT<I18nTranslations>()
|
|
57
59
|
const [open, setOpen] = React.useState(false)
|
|
58
60
|
|
|
61
|
+
const resolvedPlaceholder = placeholder ?? t('ui.datetime.pickDate')
|
|
62
|
+
|
|
59
63
|
const handleSelect: SelectSingleEventHandler = (date) => {
|
|
60
64
|
onChange?.(date)
|
|
61
65
|
setOpen(false)
|
|
@@ -75,7 +79,7 @@ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
|
75
79
|
)}
|
|
76
80
|
>
|
|
77
81
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
78
|
-
{value ? format(value, dateFormat) :
|
|
82
|
+
{value ? format(value, dateFormat) : resolvedPlaceholder}
|
|
79
83
|
</Button>
|
|
80
84
|
</PopoverTrigger>
|
|
81
85
|
<PopoverContent className="w-auto p-0" align={align}>
|
|
@@ -132,7 +136,7 @@ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps
|
|
|
132
136
|
{
|
|
133
137
|
value,
|
|
134
138
|
onChange,
|
|
135
|
-
placeholder
|
|
139
|
+
placeholder,
|
|
136
140
|
dateFormat = 'LLL dd, y',
|
|
137
141
|
disabled = false,
|
|
138
142
|
fromDate,
|
|
@@ -144,10 +148,13 @@ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps
|
|
|
144
148
|
},
|
|
145
149
|
ref
|
|
146
150
|
) => {
|
|
151
|
+
const t = useTypedT<I18nTranslations>()
|
|
147
152
|
const [open, setOpen] = React.useState(false)
|
|
148
153
|
|
|
154
|
+
const resolvedPlaceholder = placeholder ?? t('ui.datetime.pickDateRange')
|
|
155
|
+
|
|
149
156
|
const formatRange = () => {
|
|
150
|
-
if (!value?.from) return
|
|
157
|
+
if (!value?.from) return resolvedPlaceholder
|
|
151
158
|
if (!value.to) return format(value.from, dateFormat)
|
|
152
159
|
return `${format(value.from, dateFormat)} - ${format(value.to, dateFormat)}`
|
|
153
160
|
}
|
|
@@ -5,6 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { ArrowLeftIcon, ArrowRightIcon } from '@radix-ui/react-icons';
|
|
7
7
|
|
|
8
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
8
9
|
import { cn } from '../lib/utils';
|
|
9
10
|
import { Button } from './button';
|
|
10
11
|
|
|
@@ -197,7 +198,9 @@ const CarouselPrevious = React.forwardRef<
|
|
|
197
198
|
HTMLButtonElement,
|
|
198
199
|
React.ComponentProps<typeof Button>
|
|
199
200
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
201
|
+
const t = useTypedT<I18nTranslations>()
|
|
200
202
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
203
|
+
const previousLabel = t('ui.actions.previousSlide')
|
|
201
204
|
|
|
202
205
|
return (
|
|
203
206
|
<Button
|
|
@@ -216,7 +219,7 @@ const CarouselPrevious = React.forwardRef<
|
|
|
216
219
|
{...props}
|
|
217
220
|
>
|
|
218
221
|
<ArrowLeftIcon className="h-4 w-4" />
|
|
219
|
-
<span className="sr-only">
|
|
222
|
+
<span className="sr-only">{previousLabel}</span>
|
|
220
223
|
</Button>
|
|
221
224
|
)
|
|
222
225
|
})
|
|
@@ -226,7 +229,9 @@ const CarouselNext = React.forwardRef<
|
|
|
226
229
|
HTMLButtonElement,
|
|
227
230
|
React.ComponentProps<typeof Button>
|
|
228
231
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
232
|
+
const t = useTypedT<I18nTranslations>()
|
|
229
233
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
234
|
+
const nextLabel = t('ui.actions.nextSlide')
|
|
230
235
|
|
|
231
236
|
return (
|
|
232
237
|
<Button
|
|
@@ -245,7 +250,7 @@ const CarouselNext = React.forwardRef<
|
|
|
245
250
|
{...props}
|
|
246
251
|
>
|
|
247
252
|
<ArrowRightIcon className="h-4 w-4" />
|
|
248
|
-
<span className="sr-only">
|
|
253
|
+
<span className="sr-only">{nextLabel}</span>
|
|
249
254
|
</Button>
|
|
250
255
|
)
|
|
251
256
|
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { cn } from '../lib/utils';
|
|
7
8
|
import { Button } from './button';
|
|
8
9
|
import {
|
|
@@ -34,18 +35,24 @@ export function Combobox({
|
|
|
34
35
|
options,
|
|
35
36
|
value,
|
|
36
37
|
onValueChange,
|
|
37
|
-
placeholder
|
|
38
|
-
searchPlaceholder
|
|
39
|
-
emptyText
|
|
38
|
+
placeholder,
|
|
39
|
+
searchPlaceholder,
|
|
40
|
+
emptyText,
|
|
40
41
|
className,
|
|
41
42
|
disabled = false,
|
|
42
43
|
renderOption,
|
|
43
44
|
renderValue,
|
|
44
45
|
}: ComboboxProps) {
|
|
46
|
+
const t = useTypedT<I18nTranslations>()
|
|
45
47
|
const [open, setOpen] = React.useState(false)
|
|
46
48
|
const [search, setSearch] = React.useState("")
|
|
47
49
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
48
50
|
|
|
51
|
+
// Resolve translations
|
|
52
|
+
const resolvedPlaceholder = placeholder ?? t('ui.select.placeholder')
|
|
53
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.select.search')
|
|
54
|
+
const resolvedEmptyText = emptyText ?? t('ui.select.noResults')
|
|
55
|
+
|
|
49
56
|
React.useEffect(() => {
|
|
50
57
|
if (scrollRef.current && open) {
|
|
51
58
|
// Force scrollable styles with !important
|
|
@@ -102,14 +109,14 @@ export function Combobox({
|
|
|
102
109
|
? renderValue(selectedOption)
|
|
103
110
|
: selectedOption
|
|
104
111
|
? selectedOption.label
|
|
105
|
-
:
|
|
112
|
+
: resolvedPlaceholder}
|
|
106
113
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
107
114
|
</Button>
|
|
108
115
|
</PopoverTrigger>
|
|
109
116
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
|
|
110
117
|
<Command shouldFilter={false} className="flex flex-col">
|
|
111
118
|
<CommandInput
|
|
112
|
-
placeholder={
|
|
119
|
+
placeholder={resolvedSearchPlaceholder}
|
|
113
120
|
className="shrink-0"
|
|
114
121
|
value={search}
|
|
115
122
|
onValueChange={setSearch}
|
|
@@ -128,7 +135,7 @@ export function Combobox({
|
|
|
128
135
|
>
|
|
129
136
|
<CommandList className="!max-h-none !overflow-visible" style={{ pointerEvents: 'auto' }}>
|
|
130
137
|
{filteredOptions.length === 0 ? (
|
|
131
|
-
<CommandEmpty>{
|
|
138
|
+
<CommandEmpty>{resolvedEmptyText}</CommandEmpty>
|
|
132
139
|
) : (
|
|
133
140
|
<CommandGroup className="!overflow-visible" style={{ pointerEvents: 'auto' }}>
|
|
134
141
|
{filteredOptions.map((option) => (
|
package/src/components/copy.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Check, Copy } from 'lucide-react';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { cn } from '../lib/utils';
|
|
7
8
|
import { Button } from './button';
|
|
8
9
|
|
|
@@ -39,6 +40,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
39
40
|
iconClassName = 'h-4 w-4',
|
|
40
41
|
...props
|
|
41
42
|
}, ref) => {
|
|
43
|
+
const t = useTypedT<I18nTranslations>()
|
|
42
44
|
const [copied, setCopied] = React.useState(false)
|
|
43
45
|
|
|
44
46
|
const handleCopy = async () => {
|
|
@@ -53,6 +55,8 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
const hasLabel = !!children
|
|
58
|
+
const copiedText = t('ui.actions.copied')
|
|
59
|
+
const copyLabel = t('ui.actions.copyToClipboard')
|
|
56
60
|
|
|
57
61
|
return (
|
|
58
62
|
<Button
|
|
@@ -62,7 +66,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
62
66
|
variant={variant}
|
|
63
67
|
onClick={handleCopy}
|
|
64
68
|
className={cn('shrink-0', className)}
|
|
65
|
-
aria-label={copied ?
|
|
69
|
+
aria-label={copied ? copiedText : copyLabel}
|
|
66
70
|
{...props}
|
|
67
71
|
>
|
|
68
72
|
{copied ? (
|
|
@@ -70,7 +74,7 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
70
74
|
) : (
|
|
71
75
|
<Copy className={iconClassName} />
|
|
72
76
|
)}
|
|
73
|
-
{hasLabel && <span className={copied ? 'text-green-500' : undefined}>{copied ?
|
|
77
|
+
{hasLabel && <span className={copied ? 'text-green-500' : undefined}>{copied ? copiedText : children}</span>}
|
|
74
78
|
</Button>
|
|
75
79
|
)
|
|
76
80
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export { Calendar, CalendarDayButton } from './calendar';
|
|
|
62
62
|
export { DatePicker, DateRangePicker } from './calendar';
|
|
63
63
|
export type { DatePickerProps, DateRangePickerProps, DateRange } from './calendar';
|
|
64
64
|
export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from './carousel';
|
|
65
|
+
export type { CarouselApi } from './carousel';
|
|
65
66
|
export { TokenIcon, getAllTokenSymbols, searchTokens, getTokensByCategory } from './token-icon';
|
|
66
67
|
export type { TokenIconProps, TokenSymbol, TokenCategory } from './token-icon';
|
|
67
68
|
|
|
@@ -17,7 +17,7 @@ import { cva } from 'class-variance-authority';
|
|
|
17
17
|
import { Check, ChevronsUpDown, Loader2, X, XCircle } from 'lucide-react';
|
|
18
18
|
import * as React from 'react';
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
21
21
|
import { Badge } from '../badge';
|
|
22
22
|
import { Button } from '../button';
|
|
23
23
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '../command';
|
|
@@ -173,10 +173,10 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
173
173
|
searchValue,
|
|
174
174
|
onSearchChange,
|
|
175
175
|
isLoading = false,
|
|
176
|
-
placeholder
|
|
177
|
-
searchPlaceholder
|
|
178
|
-
emptyText
|
|
179
|
-
loadingText
|
|
176
|
+
placeholder,
|
|
177
|
+
searchPlaceholder,
|
|
178
|
+
emptyText,
|
|
179
|
+
loadingText,
|
|
180
180
|
variant = "default",
|
|
181
181
|
animation = 0,
|
|
182
182
|
animationConfig,
|
|
@@ -197,6 +197,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
197
197
|
},
|
|
198
198
|
ref
|
|
199
199
|
) => {
|
|
200
|
+
const t = useTypedT<I18nTranslations>()
|
|
200
201
|
const [open, setOpen] = React.useState(false)
|
|
201
202
|
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
|
|
202
203
|
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
|
@@ -204,6 +205,18 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
204
205
|
// Cache selected options to persist them when they disappear from current options
|
|
205
206
|
const [selectedOptionsCache, setSelectedOptionsCache] = React.useState<Map<string, MultiSelectProAsyncOption>>(new Map())
|
|
206
207
|
|
|
208
|
+
// Prepare translations
|
|
209
|
+
const translations = React.useMemo(() => ({
|
|
210
|
+
placeholder: placeholder ?? t('ui.select.placeholder'),
|
|
211
|
+
search: searchPlaceholder ?? t('ui.select.search'),
|
|
212
|
+
selectAll: t('ui.select.selectAll'),
|
|
213
|
+
clearAll: t('ui.select.clearAll'),
|
|
214
|
+
noResults: emptyText ?? t('ui.select.noResults'),
|
|
215
|
+
loading: loadingText ?? t('ui.select.loading'),
|
|
216
|
+
moreItems: (count: number) => t('ui.select.moreItems', { count }),
|
|
217
|
+
remove: (label: string) => t('ui.actions.remove', { item: label }),
|
|
218
|
+
}), [placeholder, searchPlaceholder, emptyText, loadingText, t])
|
|
219
|
+
|
|
207
220
|
// Process options
|
|
208
221
|
const flatOptions = React.useMemo(() => {
|
|
209
222
|
const flat = flattenOptions(options)
|
|
@@ -373,7 +386,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
373
386
|
e.stopPropagation()
|
|
374
387
|
toggleOption(option.value)
|
|
375
388
|
}}
|
|
376
|
-
aria-label={
|
|
389
|
+
aria-label={translations.remove(option.label)}
|
|
377
390
|
>
|
|
378
391
|
<X className="h-3 w-3" />
|
|
379
392
|
</button>
|
|
@@ -385,7 +398,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
385
398
|
// Display value
|
|
386
399
|
const displayValue = React.useMemo(() => {
|
|
387
400
|
if (selectedOptions.length === 0) {
|
|
388
|
-
return <span className="text-muted-foreground">{placeholder}</span>
|
|
401
|
+
return <span className="text-muted-foreground">{translations.placeholder}</span>
|
|
389
402
|
}
|
|
390
403
|
|
|
391
404
|
const displayed = selectedOptions.slice(0, maxCount)
|
|
@@ -396,12 +409,12 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
396
409
|
{displayed.map((option, index) => renderBadge(option, index))}
|
|
397
410
|
{remaining > 0 && (
|
|
398
411
|
<Badge variant="outline" className="text-xs">
|
|
399
|
-
|
|
412
|
+
{translations.moreItems(remaining)}
|
|
400
413
|
</Badge>
|
|
401
414
|
)}
|
|
402
415
|
</div>
|
|
403
416
|
)
|
|
404
|
-
}, [selectedOptions, maxCount,
|
|
417
|
+
}, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
|
|
405
418
|
|
|
406
419
|
// Render options
|
|
407
420
|
const renderOptions = () => {
|
|
@@ -535,7 +548,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
535
548
|
handleClearAll()
|
|
536
549
|
}}
|
|
537
550
|
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
538
|
-
aria-label=
|
|
551
|
+
aria-label={translations.clearAll}
|
|
539
552
|
>
|
|
540
553
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
541
554
|
</button>
|
|
@@ -558,7 +571,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
558
571
|
<Command shouldFilter={false}>
|
|
559
572
|
{searchable && (
|
|
560
573
|
<CommandInput
|
|
561
|
-
placeholder={
|
|
574
|
+
placeholder={translations.search}
|
|
562
575
|
value={searchValue}
|
|
563
576
|
onValueChange={onSearchChange}
|
|
564
577
|
/>
|
|
@@ -571,7 +584,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
571
584
|
onSelect={handleSelectAll}
|
|
572
585
|
className="cursor-pointer justify-center font-medium"
|
|
573
586
|
>
|
|
574
|
-
|
|
587
|
+
{translations.selectAll}
|
|
575
588
|
</CommandItem>
|
|
576
589
|
</CommandGroup>
|
|
577
590
|
<Separator />
|
|
@@ -581,10 +594,10 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
581
594
|
{isLoading ? (
|
|
582
595
|
<div className="py-6 text-center text-sm">
|
|
583
596
|
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
|
|
584
|
-
{
|
|
597
|
+
{translations.loading}
|
|
585
598
|
</div>
|
|
586
599
|
) : options.length === 0 ? (
|
|
587
|
-
<CommandEmpty>{
|
|
600
|
+
<CommandEmpty>{translations.noResults}</CommandEmpty>
|
|
588
601
|
) : (
|
|
589
602
|
renderOptions()
|
|
590
603
|
)}
|
|
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
import { Check, ChevronsUpDown, X, XCircle } from 'lucide-react';
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
7
8
|
import { Badge } from '../badge';
|
|
8
9
|
import { Button } from '../button';
|
|
9
10
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '../command';
|
|
@@ -150,7 +151,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
150
151
|
options,
|
|
151
152
|
onValueChange,
|
|
152
153
|
defaultValue = [],
|
|
153
|
-
placeholder
|
|
154
|
+
placeholder,
|
|
154
155
|
variant = "default",
|
|
155
156
|
animation = 0,
|
|
156
157
|
animationConfig,
|
|
@@ -173,6 +174,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
173
174
|
},
|
|
174
175
|
ref
|
|
175
176
|
) => {
|
|
177
|
+
const t = useTypedT<I18nTranslations>()
|
|
176
178
|
const [open, setOpen] = React.useState(false)
|
|
177
179
|
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
|
|
178
180
|
const [search, setSearch] = React.useState("")
|
|
@@ -181,6 +183,17 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
181
183
|
// Cache selected options to persist them when they disappear from current options
|
|
182
184
|
const [selectedOptionsCache, setSelectedOptionsCache] = React.useState<Map<string, MultiSelectProOption>>(new Map())
|
|
183
185
|
|
|
186
|
+
// Prepare translations
|
|
187
|
+
const translations = React.useMemo(() => ({
|
|
188
|
+
placeholder: placeholder ?? t('ui.select.placeholder'),
|
|
189
|
+
search: t('ui.select.search'),
|
|
190
|
+
selectAll: t('ui.select.selectAll'),
|
|
191
|
+
clearAll: t('ui.select.clearAll'),
|
|
192
|
+
noResults: t('ui.select.noResults'),
|
|
193
|
+
moreItems: (count: number) => t('ui.select.moreItems', { count }),
|
|
194
|
+
remove: (label: string) => t('ui.actions.remove', { item: label }),
|
|
195
|
+
}), [placeholder, t])
|
|
196
|
+
|
|
184
197
|
// Process options
|
|
185
198
|
const flatOptions = React.useMemo(() => {
|
|
186
199
|
const flat = flattenOptions(options)
|
|
@@ -389,7 +402,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
389
402
|
e.stopPropagation()
|
|
390
403
|
toggleOption(option.value)
|
|
391
404
|
}}
|
|
392
|
-
aria-label={
|
|
405
|
+
aria-label={translations.remove(option.label)}
|
|
393
406
|
>
|
|
394
407
|
<X className="h-3 w-3" />
|
|
395
408
|
</button>
|
|
@@ -401,7 +414,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
401
414
|
// Display value
|
|
402
415
|
const displayValue = React.useMemo(() => {
|
|
403
416
|
if (selectedOptions.length === 0) {
|
|
404
|
-
return <span className="text-muted-foreground">{placeholder}</span>
|
|
417
|
+
return <span className="text-muted-foreground">{translations.placeholder}</span>
|
|
405
418
|
}
|
|
406
419
|
|
|
407
420
|
const displayed = selectedOptions.slice(0, maxCount)
|
|
@@ -412,12 +425,12 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
412
425
|
{displayed.map((option, index) => renderBadge(option, index))}
|
|
413
426
|
{remaining > 0 && (
|
|
414
427
|
<Badge variant="outline" className="text-xs">
|
|
415
|
-
|
|
428
|
+
{translations.moreItems(remaining)}
|
|
416
429
|
</Badge>
|
|
417
430
|
)}
|
|
418
431
|
</div>
|
|
419
432
|
)
|
|
420
|
-
}, [selectedOptions, maxCount,
|
|
433
|
+
}, [selectedOptions, maxCount, translations, singleLine, variant, disabled, animConfig])
|
|
421
434
|
|
|
422
435
|
// Render options
|
|
423
436
|
const renderOptions = () => {
|
|
@@ -551,7 +564,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
551
564
|
handleClearAll()
|
|
552
565
|
}}
|
|
553
566
|
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
554
|
-
aria-label=
|
|
567
|
+
aria-label={translations.clearAll}
|
|
555
568
|
>
|
|
556
569
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
557
570
|
</button>
|
|
@@ -574,7 +587,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
574
587
|
<Command shouldFilter={false}>
|
|
575
588
|
{searchable && (
|
|
576
589
|
<CommandInput
|
|
577
|
-
placeholder=
|
|
590
|
+
placeholder={translations.search}
|
|
578
591
|
value={search}
|
|
579
592
|
onValueChange={setSearch}
|
|
580
593
|
/>
|
|
@@ -587,7 +600,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
587
600
|
onSelect={handleSelectAll}
|
|
588
601
|
className="cursor-pointer justify-center font-medium"
|
|
589
602
|
>
|
|
590
|
-
|
|
603
|
+
{translations.selectAll}
|
|
591
604
|
</CommandItem>
|
|
592
605
|
</CommandGroup>
|
|
593
606
|
<Separator />
|
|
@@ -596,7 +609,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
596
609
|
|
|
597
610
|
{filteredOptions.length === 0 ? (
|
|
598
611
|
<CommandEmpty>
|
|
599
|
-
{emptyIndicator ||
|
|
612
|
+
{emptyIndicator || translations.noResults}
|
|
600
613
|
</CommandEmpty>
|
|
601
614
|
) : (
|
|
602
615
|
renderOptions()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { cn } from '../lib/utils';
|
|
7
8
|
import { Badge } from './badge';
|
|
8
9
|
import { Button } from './button';
|
|
@@ -34,17 +35,23 @@ export function MultiSelect({
|
|
|
34
35
|
options,
|
|
35
36
|
value = [],
|
|
36
37
|
onChange,
|
|
37
|
-
placeholder
|
|
38
|
-
searchPlaceholder
|
|
39
|
-
emptyText
|
|
38
|
+
placeholder,
|
|
39
|
+
searchPlaceholder,
|
|
40
|
+
emptyText,
|
|
40
41
|
className,
|
|
41
42
|
disabled = false,
|
|
42
43
|
maxDisplay = 3,
|
|
43
44
|
}: MultiSelectProps) {
|
|
45
|
+
const t = useTypedT<I18nTranslations>()
|
|
44
46
|
const [open, setOpen] = React.useState(false)
|
|
45
47
|
const [search, setSearch] = React.useState("")
|
|
46
48
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
47
49
|
|
|
50
|
+
// Resolve translations
|
|
51
|
+
const resolvedPlaceholder = placeholder ?? t('ui.select.placeholder')
|
|
52
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.select.search')
|
|
53
|
+
const resolvedEmptyText = emptyText ?? t('ui.select.noResults')
|
|
54
|
+
|
|
48
55
|
React.useEffect(() => {
|
|
49
56
|
if (scrollRef.current && open) {
|
|
50
57
|
const el = scrollRef.current
|
|
@@ -88,7 +95,7 @@ export function MultiSelect({
|
|
|
88
95
|
|
|
89
96
|
const displayValue = React.useMemo(() => {
|
|
90
97
|
if (selectedOptions.length === 0) {
|
|
91
|
-
return <span className="text-muted-foreground">{
|
|
98
|
+
return <span className="text-muted-foreground">{resolvedPlaceholder}</span>
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
const displayed = selectedOptions.slice(0, maxDisplay)
|
|
@@ -107,6 +114,7 @@ export function MultiSelect({
|
|
|
107
114
|
className="ml-1 rounded-full hover:bg-muted-foreground/20"
|
|
108
115
|
onClick={(e) => handleRemove(option.value, e)}
|
|
109
116
|
disabled={disabled}
|
|
117
|
+
aria-label={t('ui.actions.remove', { item: option.label })}
|
|
110
118
|
>
|
|
111
119
|
<X className="h-3 w-3" />
|
|
112
120
|
</button>
|
|
@@ -114,12 +122,12 @@ export function MultiSelect({
|
|
|
114
122
|
))}
|
|
115
123
|
{remaining > 0 && (
|
|
116
124
|
<Badge variant="outline" className="text-xs">
|
|
117
|
-
|
|
125
|
+
{t('ui.select.moreItems', { count: remaining })}
|
|
118
126
|
</Badge>
|
|
119
127
|
)}
|
|
120
128
|
</div>
|
|
121
129
|
)
|
|
122
|
-
}, [selectedOptions, maxDisplay,
|
|
130
|
+
}, [selectedOptions, maxDisplay, resolvedPlaceholder, disabled, t])
|
|
123
131
|
|
|
124
132
|
return (
|
|
125
133
|
<Popover
|
|
@@ -151,7 +159,7 @@ export function MultiSelect({
|
|
|
151
159
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
|
|
152
160
|
<Command shouldFilter={false} className="flex flex-col">
|
|
153
161
|
<CommandInput
|
|
154
|
-
placeholder={
|
|
162
|
+
placeholder={resolvedSearchPlaceholder}
|
|
155
163
|
className="shrink-0"
|
|
156
164
|
value={search}
|
|
157
165
|
onValueChange={setSearch}
|
|
@@ -170,7 +178,7 @@ export function MultiSelect({
|
|
|
170
178
|
>
|
|
171
179
|
<CommandList className="!max-h-none !overflow-visible" style={{ pointerEvents: 'auto' }}>
|
|
172
180
|
{filteredOptions.length === 0 ? (
|
|
173
|
-
<CommandEmpty>{
|
|
181
|
+
<CommandEmpty>{resolvedEmptyText}</CommandEmpty>
|
|
174
182
|
) : (
|
|
175
183
|
<CommandGroup className="!overflow-visible" style={{ pointerEvents: 'auto' }}>
|
|
176
184
|
{filteredOptions.map((option) => {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { ChevronDown, Search } from 'lucide-react';
|
|
7
7
|
import * as React from 'react';
|
|
8
8
|
|
|
9
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
9
10
|
import { Button } from './button';
|
|
10
11
|
import { Input } from './input';
|
|
11
12
|
import { cn } from '../lib';
|
|
@@ -58,16 +59,23 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
58
59
|
value = '',
|
|
59
60
|
onChange,
|
|
60
61
|
defaultCountry = 'US',
|
|
61
|
-
placeholder
|
|
62
|
+
placeholder,
|
|
62
63
|
disabled = false,
|
|
63
64
|
...props
|
|
64
65
|
}, ref) => {
|
|
66
|
+
const t = useTypedT<I18nTranslations>()
|
|
65
67
|
const [selectedCountry, setSelectedCountry] = React.useState<CountryCode>(defaultCountry)
|
|
66
68
|
const [inputValue, setInputValue] = React.useState('')
|
|
67
69
|
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
|
68
70
|
const [searchQuery, setSearchQuery] = React.useState('')
|
|
69
71
|
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
|
70
72
|
|
|
73
|
+
// Prepare translations
|
|
74
|
+
const translations = React.useMemo(() => ({
|
|
75
|
+
searchCountries: t('ui.phone.searchCountries'),
|
|
76
|
+
noCountries: t('ui.phone.noCountries'),
|
|
77
|
+
}), [t])
|
|
78
|
+
|
|
71
79
|
// Find country data
|
|
72
80
|
const currentCountry = COUNTRIES.find(c => c.code === selectedCountry) || COUNTRIES[0]!
|
|
73
81
|
|
|
@@ -218,7 +226,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
218
226
|
<div className="relative">
|
|
219
227
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
220
228
|
<Input
|
|
221
|
-
placeholder=
|
|
229
|
+
placeholder={translations.searchCountries}
|
|
222
230
|
value={searchQuery}
|
|
223
231
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
224
232
|
className="pl-8 h-8"
|
|
@@ -231,7 +239,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
231
239
|
<div className="max-h-60 overflow-y-auto">
|
|
232
240
|
{filteredCountries.length === 0 ? (
|
|
233
241
|
<div className="p-4 text-sm text-muted-foreground text-center">
|
|
234
|
-
|
|
242
|
+
{translations.noCountries}
|
|
235
243
|
</div>
|
|
236
244
|
) : (
|
|
237
245
|
filteredCountries.map((country, index) => (
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { Loader2 } from 'lucide-react';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
|
|
13
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
13
14
|
import { cn } from '../lib/utils';
|
|
14
15
|
import { Spinner } from './spinner';
|
|
15
16
|
|
|
@@ -102,6 +103,8 @@ export function Preloader({
|
|
|
102
103
|
className,
|
|
103
104
|
spinnerClassName,
|
|
104
105
|
}: PreloaderProps) {
|
|
106
|
+
const t = useTypedT<I18nTranslations>()
|
|
107
|
+
const loadingLabel = text || t('ui.states.loading')
|
|
105
108
|
const spinnerSize = sizeMap[size];
|
|
106
109
|
|
|
107
110
|
// Fullscreen variant
|
|
@@ -124,7 +127,7 @@ export function Preloader({
|
|
|
124
127
|
className
|
|
125
128
|
)}
|
|
126
129
|
role="status"
|
|
127
|
-
aria-label={
|
|
130
|
+
aria-label={loadingLabel}
|
|
128
131
|
aria-live="polite"
|
|
129
132
|
>
|
|
130
133
|
<div className="flex flex-col items-center gap-4 animate-in fade-in duration-300">
|
|
@@ -176,7 +179,7 @@ export function Preloader({
|
|
|
176
179
|
className
|
|
177
180
|
)}
|
|
178
181
|
role="status"
|
|
179
|
-
aria-label={
|
|
182
|
+
aria-label={loadingLabel}
|
|
180
183
|
aria-live="polite"
|
|
181
184
|
>
|
|
182
185
|
<Loader2
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Loader2Icon } from 'lucide-react';
|
|
2
2
|
|
|
3
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
3
4
|
import { cn } from '../lib/utils';
|
|
4
5
|
|
|
5
6
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|
7
|
+
const t = useTypedT<I18nTranslations>()
|
|
8
|
+
const loadingLabel = t('ui.states.loading')
|
|
9
|
+
|
|
6
10
|
return (
|
|
7
11
|
<Loader2Icon
|
|
8
12
|
role="status"
|
|
9
|
-
aria-label=
|
|
13
|
+
aria-label={loadingLabel}
|
|
10
14
|
className={cn("size-4 animate-spin", className)}
|
|
11
15
|
{...props}
|
|
12
16
|
/>
|
package/src/components/tabs.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import * as React from 'react';
|
|
|
6
6
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
7
7
|
|
|
8
8
|
import { useIsMobile } from '../hooks';
|
|
9
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
9
10
|
import { cn } from '../lib/utils';
|
|
10
11
|
import { Button } from './button';
|
|
11
12
|
import { ScrollArea, ScrollBar } from './scroll-area';
|
|
@@ -40,7 +41,9 @@ interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.
|
|
|
40
41
|
const Tabs = React.forwardRef<
|
|
41
42
|
React.ElementRef<typeof TabsPrimitive.Root>,
|
|
42
43
|
TabsProps
|
|
43
|
-
>(({ mobileSheet = false, mobileSheetTitle
|
|
44
|
+
>(({ mobileSheet = false, mobileSheetTitle, mobileTitleText, sticky = false, children, ...props }, ref) => {
|
|
45
|
+
const t = useTypedT<I18nTranslations>()
|
|
46
|
+
const resolvedMobileSheetTitle = mobileSheetTitle ?? t('ui.navigation.title')
|
|
44
47
|
const isMobile = useIsMobile()
|
|
45
48
|
const [open, setOpen] = React.useState(false)
|
|
46
49
|
|
|
@@ -109,7 +112,7 @@ const Tabs = React.forwardRef<
|
|
|
109
112
|
</SheetTrigger>
|
|
110
113
|
<SheetContent side="right" className="w-[280px] sm:w-[350px]">
|
|
111
114
|
<SheetHeader>
|
|
112
|
-
<SheetTitle>{
|
|
115
|
+
<SheetTitle>{resolvedMobileSheetTitle}</SheetTitle>
|
|
113
116
|
</SheetHeader>
|
|
114
117
|
<nav className="flex flex-col gap-2 mt-6">
|
|
115
118
|
<TabsPrimitive.List className="flex flex-col gap-2">
|