@djangocfg/layouts 2.1.304 → 2.1.308

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/package.json +18 -18
  3. package/src/layouts/AppLayout/AppLayout.tsx +14 -11
  4. package/src/layouts/AppLayout/BaseApp.tsx +3 -1
  5. package/src/layouts/AppLayout/LayoutI18nProvider.tsx +59 -0
  6. package/src/layouts/AppLayout/index.ts +7 -0
  7. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -5
  8. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +2 -4
  9. package/src/layouts/PublicLayout/README.md +13 -13
  10. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +6 -11
  11. package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +15 -6
  12. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +1 -5
  13. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +1 -5
  14. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -6
  15. package/src/layouts/PublicLayout/primitives/NavControls.tsx +5 -9
  16. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +2 -6
  17. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +1 -7
  18. package/src/layouts/_components/LocaleSwitcher.tsx +40 -178
  19. package/src/layouts/_components/PrivateSidebarAccount.tsx +6 -8
  20. package/src/layouts/_components/UserMenu.tsx +15 -19
  21. package/src/layouts/_components/index.ts +23 -2
  22. package/src/layouts/_components/locale-switcher/LocaleCard.tsx +91 -0
  23. package/src/layouts/_components/locale-switcher/LocaleGrid.tsx +128 -0
  24. package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +100 -0
  25. package/src/layouts/_components/locale-switcher/LocaleSwitcherDialog.tsx +168 -0
  26. package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +101 -0
  27. package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +103 -0
  28. package/src/layouts/_components/locale-switcher/index.ts +27 -0
  29. package/src/layouts/_components/locale-switcher/localeMeta.ts +109 -0
  30. package/src/layouts/_components/locale-switcher/types.ts +48 -0
  31. package/src/layouts/types/layout.types.ts +37 -1
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Search, X } from 'lucide-react';
5
+
6
+ import { Input } from '@djangocfg/ui-core/components';
7
+ import { cn } from '@djangocfg/ui-core/lib';
8
+
9
+ import { LocaleCard } from './LocaleCard';
10
+ import { getLocaleMeta, type LocaleMeta } from './localeMeta';
11
+
12
+ export interface LocaleGridProps {
13
+ locale: string;
14
+ locales: string[];
15
+ onSelect: (locale: string) => void;
16
+ labels?: Record<string, string>;
17
+ searchPlaceholder?: string;
18
+ emptyResults?: string;
19
+ /** Force compact (single-column list) layout. @default false (grid on ≥sm) */
20
+ compact?: boolean;
21
+ /** Search input visibility. Auto-shown when more than `searchThreshold` locales. */
22
+ searchThreshold?: number;
23
+ }
24
+
25
+ interface LocaleEntry {
26
+ code: string;
27
+ meta: LocaleMeta;
28
+ }
29
+
30
+ const DEFAULT_SEARCH_THRESHOLD = 8;
31
+
32
+ export function LocaleGrid({
33
+ locale,
34
+ locales,
35
+ onSelect,
36
+ labels,
37
+ searchPlaceholder = 'Search language…',
38
+ emptyResults = 'No languages found',
39
+ compact = false,
40
+ searchThreshold = DEFAULT_SEARCH_THRESHOLD,
41
+ }: LocaleGridProps) {
42
+ const [query, setQuery] = React.useState('');
43
+
44
+ const entries = React.useMemo<LocaleEntry[]>(
45
+ () =>
46
+ locales.map((code) => ({
47
+ code,
48
+ meta: getLocaleMeta(code, labels),
49
+ })),
50
+ [locales, labels],
51
+ );
52
+
53
+ const showSearch = entries.length > searchThreshold;
54
+
55
+ const filtered = React.useMemo(() => {
56
+ if (!query.trim()) return entries;
57
+ const q = query.trim().toLowerCase();
58
+ return entries.filter(
59
+ (e) =>
60
+ e.code.toLowerCase().includes(q) ||
61
+ e.meta.native.toLowerCase().includes(q) ||
62
+ e.meta.english.toLowerCase().includes(q),
63
+ );
64
+ }, [entries, query]);
65
+
66
+ return (
67
+ <div className="flex h-full min-h-0 flex-col gap-4">
68
+ {showSearch && (
69
+ // `p-px` reserves the focus-ring offset so it isn't clipped by the
70
+ // scroll container below.
71
+ <div className="relative shrink-0 p-px">
72
+ <Search
73
+ className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
74
+ aria-hidden
75
+ />
76
+ <Input
77
+ type="search"
78
+ value={query}
79
+ onChange={(e) => setQuery(e.target.value)}
80
+ placeholder={searchPlaceholder}
81
+ className="h-11 rounded-xl pl-10 pr-9"
82
+ // Avoid forcing the keyboard open on mobile.
83
+ autoFocus={false}
84
+ />
85
+ {query && (
86
+ <button
87
+ type="button"
88
+ onClick={() => setQuery('')}
89
+ aria-label="Clear search"
90
+ className="absolute right-2.5 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
91
+ >
92
+ <X className="h-3.5 w-3.5" />
93
+ </button>
94
+ )}
95
+ </div>
96
+ )}
97
+
98
+ {/* `p-px` reserves the focus / active ring offset around cards. */}
99
+ <div className="flex-1 min-h-0 overflow-y-auto p-px">
100
+ {filtered.length === 0 ? (
101
+ <div className="flex h-full items-center justify-center py-12 text-sm text-muted-foreground">
102
+ {emptyResults}
103
+ </div>
104
+ ) : (
105
+ <div
106
+ className={cn(
107
+ 'grid gap-2.5',
108
+ compact
109
+ ? 'grid-cols-1'
110
+ : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
111
+ )}
112
+ >
113
+ {filtered.map(({ code, meta }) => (
114
+ <LocaleCard
115
+ key={code}
116
+ code={code}
117
+ meta={meta}
118
+ active={code === locale}
119
+ onSelect={onSelect}
120
+ compact={compact}
121
+ />
122
+ ))}
123
+ </div>
124
+ )}
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ import { useLayoutI18n } from '../../AppLayout/LayoutI18nProvider';
6
+
7
+ import { LocaleSwitcherDialog } from './LocaleSwitcherDialog';
8
+ import { LocaleSwitcherDropdown } from './LocaleSwitcherDropdown';
9
+ import { LocaleSwitcherTrigger } from './LocaleSwitcherTrigger';
10
+ import type { LocaleSwitcherVariant } from './types';
11
+
12
+ export interface LocaleSwitcherProps {
13
+ /**
14
+ * UI flavour.
15
+ * - `dialog` (default): fullscreen language picker with brand header, search, grid.
16
+ * - `dropdown`: compact dropdown menu — best for navbars and dense rails.
17
+ */
18
+ variant?: LocaleSwitcherVariant;
19
+ /** Override / add native names per locale (`{ es: 'Español' }`). */
20
+ labels?: Record<string, string>;
21
+
22
+ // Trigger styling — applies to both variants.
23
+ showCode?: boolean;
24
+ buttonVariant?: 'ghost' | 'outline' | 'default';
25
+ size?: 'sm' | 'default' | 'lg' | 'icon';
26
+ /** Show the leading Globe icon when no flag is rendered. @default true */
27
+ showIcon?: boolean;
28
+ /** Show the country flag in the trigger. @default true */
29
+ showFlag?: boolean;
30
+ /** Show the label text in the trigger. @default true */
31
+ showTriggerLabel?: boolean;
32
+ /** Extra trigger className. */
33
+ className?: string;
34
+ }
35
+
36
+ /**
37
+ * Locale switcher with two flavours: a compact dropdown and a fullscreen
38
+ * dialog. Pulls `locale` / `locales` / `onChange` / `brand` / `dialogLabels`
39
+ * from `LayoutI18nProvider` (mounted by `BaseApp`) — no need to thread them
40
+ * through props.
41
+ */
42
+ export function LocaleSwitcher({
43
+ variant = 'dialog',
44
+ labels,
45
+ className,
46
+ showCode = false,
47
+ buttonVariant = 'ghost',
48
+ size = 'sm',
49
+ showIcon = true,
50
+ showFlag = true,
51
+ showTriggerLabel = true,
52
+ }: LocaleSwitcherProps) {
53
+ const i18n = useLayoutI18n();
54
+ const [open, setOpen] = React.useState(false);
55
+
56
+ if (variant === 'dropdown') {
57
+ return (
58
+ <LocaleSwitcherDropdown
59
+ locale={i18n.locale}
60
+ locales={i18n.locales}
61
+ onChange={i18n.onLocaleChange}
62
+ labels={labels}
63
+ className={className}
64
+ showCode={showCode}
65
+ variant={buttonVariant}
66
+ size={size}
67
+ showIcon={showIcon}
68
+ showFlag={showFlag}
69
+ showTriggerLabel={showTriggerLabel}
70
+ />
71
+ );
72
+ }
73
+
74
+ return (
75
+ <>
76
+ <LocaleSwitcherTrigger
77
+ locale={i18n.locale}
78
+ labels={labels}
79
+ showFlag={showFlag}
80
+ showLabel={showTriggerLabel}
81
+ showCode={showCode}
82
+ variant={buttonVariant}
83
+ size={size}
84
+ className={className}
85
+ ariaLabel={i18n.dialogLabels?.triggerLabel ?? 'Change language'}
86
+ onClick={() => setOpen(true)}
87
+ />
88
+ <LocaleSwitcherDialog
89
+ locale={i18n.locale}
90
+ locales={i18n.locales}
91
+ onChange={i18n.onLocaleChange}
92
+ labels={labels}
93
+ i18nLabels={i18n.dialogLabels}
94
+ brand={i18n.brand}
95
+ open={open}
96
+ onOpenChange={setOpen}
97
+ />
98
+ </>
99
+ );
100
+ }
@@ -0,0 +1,168 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Globe, X } from 'lucide-react';
5
+
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogTitle,
10
+ DialogClose,
11
+ } from '@djangocfg/ui-core/components';
12
+ import { useIsMobile } from '@djangocfg/ui-core/hooks';
13
+ import { cn } from '@djangocfg/ui-core/lib';
14
+
15
+ import { LocaleGrid } from './LocaleGrid';
16
+ import type {
17
+ LocaleSwitcherBrand,
18
+ LocaleSwitcherLabels,
19
+ LocaleSwitcherSharedProps,
20
+ } from './types';
21
+
22
+ export interface LocaleSwitcherDialogProps extends LocaleSwitcherSharedProps {
23
+ open: boolean;
24
+ onOpenChange: (open: boolean) => void;
25
+ brand?: LocaleSwitcherBrand;
26
+ }
27
+
28
+ const DEFAULT_LABELS: Required<LocaleSwitcherLabels> = {
29
+ dialogTitle: 'Choose your language',
30
+ dialogSubtitle: 'Pick the language you want to browse the site in.',
31
+ searchPlaceholder: 'Search language…',
32
+ emptyResults: 'No languages found',
33
+ closeLabel: 'Close',
34
+ triggerLabel: 'Change language',
35
+ };
36
+
37
+ /**
38
+ * Fullscreen language picker dialog. Slides up from the bottom on mobile,
39
+ * fades into a centred fullscreen sheet on tablet/desktop.
40
+ *
41
+ * The dialog uses ui-core's `<DialogContent fullscreen>` and provides a
42
+ * custom pill-shaped close button via `closeButton` so the default Radix
43
+ * X glyph does not stack on top.
44
+ */
45
+ export function LocaleSwitcherDialog({
46
+ locale,
47
+ locales,
48
+ onChange,
49
+ open,
50
+ onOpenChange,
51
+ brand,
52
+ labels,
53
+ i18nLabels,
54
+ }: LocaleSwitcherDialogProps) {
55
+ const isMobile = useIsMobile();
56
+
57
+ const merged = React.useMemo(
58
+ () => ({ ...DEFAULT_LABELS, ...i18nLabels }),
59
+ [i18nLabels],
60
+ );
61
+
62
+ const handleSelect = React.useCallback(
63
+ (code: string) => {
64
+ onChange(code);
65
+ onOpenChange(false);
66
+ },
67
+ [onChange, onOpenChange],
68
+ );
69
+
70
+ const closeButton = React.useMemo(
71
+ () => (
72
+ <DialogClose
73
+ aria-label={merged.closeLabel}
74
+ className={cn(
75
+ 'absolute right-4 top-4 inline-flex h-10 w-10 items-center justify-center',
76
+ 'cursor-pointer rounded-full border border-border/60 bg-background/90 text-muted-foreground shadow-sm',
77
+ 'transition-colors hover:bg-accent hover:text-foreground',
78
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
79
+ )}
80
+ >
81
+ <X className="h-4 w-4" />
82
+ </DialogClose>
83
+ ),
84
+ [merged.closeLabel],
85
+ );
86
+
87
+ return (
88
+ <Dialog open={open} onOpenChange={onOpenChange}>
89
+ <DialogContent
90
+ fullscreen
91
+ closeButton={closeButton}
92
+ className={cn(
93
+ 'bg-background/95 backdrop-blur-md',
94
+ isMobile
95
+ ? 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom'
96
+ : null,
97
+ )}
98
+ >
99
+ <DialogTitle className="sr-only">{merged.dialogTitle}</DialogTitle>
100
+
101
+ {/*
102
+ Single padded shell — both header and grid use the same horizontal
103
+ rhythm, so the search input lines up with the cards beneath it.
104
+ The shell scrolls; the grid below uses overflow-hidden so the focus
105
+ ring on the search input isn't clipped by an inner scroll edge.
106
+ */}
107
+ <div className="mx-auto flex h-full w-full max-w-5xl flex-col px-4 sm:px-6 lg:px-8">
108
+ <LocaleSwitcherDialogHeader brand={brand} labels={merged} />
109
+
110
+ <div className="flex-1 min-h-0 pb-6 lg:pb-10">
111
+ <LocaleGrid
112
+ locale={locale}
113
+ locales={locales}
114
+ onSelect={handleSelect}
115
+ labels={labels}
116
+ searchPlaceholder={merged.searchPlaceholder}
117
+ emptyResults={merged.emptyResults}
118
+ compact={isMobile}
119
+ />
120
+ </div>
121
+ </div>
122
+ </DialogContent>
123
+ </Dialog>
124
+ );
125
+ }
126
+
127
+ interface DialogHeaderProps {
128
+ brand?: LocaleSwitcherBrand;
129
+ labels: Required<LocaleSwitcherLabels>;
130
+ }
131
+
132
+ function LocaleSwitcherDialogHeader({ brand, labels }: DialogHeaderProps) {
133
+ const hasBrand = Boolean(brand?.logo || brand?.name);
134
+
135
+ return (
136
+ <header className="shrink-0 pb-5 pt-6 sm:pt-8 lg:pb-7 lg:pt-10">
137
+ <div className="mb-5 inline-flex items-center gap-2.5 lg:mb-7">
138
+ {hasBrand ? (
139
+ <>
140
+ {brand?.logo ? (
141
+ <span className="inline-flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary/10 text-primary">
142
+ {brand.logo}
143
+ </span>
144
+ ) : null}
145
+ {brand?.name ? (
146
+ <span className="text-sm font-semibold tracking-tight text-foreground">
147
+ {brand.name}
148
+ </span>
149
+ ) : null}
150
+ </>
151
+ ) : (
152
+ <span className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
153
+ <Globe className="h-4 w-4" aria-hidden />
154
+ </span>
155
+ )}
156
+ </div>
157
+
158
+ <h2 className="text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
159
+ {labels.dialogTitle}
160
+ </h2>
161
+ {labels.dialogSubtitle && (
162
+ <p className="mt-1.5 text-sm text-muted-foreground sm:text-base">
163
+ {labels.dialogSubtitle}
164
+ </p>
165
+ )}
166
+ </header>
167
+ );
168
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Globe } from 'lucide-react';
5
+
6
+ import {
7
+ Button,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ LanguageFlag,
13
+ } from '@djangocfg/ui-core/components';
14
+ import { cn } from '@djangocfg/ui-core/lib';
15
+
16
+ import { getLocaleMeta } from './localeMeta';
17
+ import type { LocaleSwitcherSharedProps } from './types';
18
+
19
+ export interface LocaleSwitcherDropdownProps extends LocaleSwitcherSharedProps {
20
+ showCode?: boolean;
21
+ variant?: 'ghost' | 'outline' | 'default';
22
+ size?: 'sm' | 'default' | 'lg' | 'icon';
23
+ showIcon?: boolean;
24
+ showFlag?: boolean;
25
+ showTriggerLabel?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Compact dropdown locale switcher — kept for navbar and dense layouts where
30
+ * a fullscreen dialog feels heavy.
31
+ */
32
+ export function LocaleSwitcherDropdown({
33
+ locale,
34
+ locales,
35
+ onChange,
36
+ labels,
37
+ showCode = false,
38
+ variant = 'ghost',
39
+ size = 'sm',
40
+ showIcon = true,
41
+ showFlag = true,
42
+ showTriggerLabel = true,
43
+ className,
44
+ }: LocaleSwitcherDropdownProps) {
45
+ const currentMeta = getLocaleMeta(locale, labels);
46
+ const currentLabel = showCode ? locale.toUpperCase() : currentMeta.native;
47
+ const isIconOnly = size === 'icon';
48
+
49
+ return (
50
+ <DropdownMenu>
51
+ <DropdownMenuTrigger asChild>
52
+ {isIconOnly ? (
53
+ <Button
54
+ variant={variant}
55
+ size={size}
56
+ aria-label={currentLabel}
57
+ className={cn('overflow-hidden rounded-full p-0', className)}
58
+ >
59
+ {showFlag ? (
60
+ <LanguageFlag code={locale} className="h-full w-full object-cover" />
61
+ ) : (
62
+ <Globe className="h-4 w-4" aria-hidden />
63
+ )}
64
+ </Button>
65
+ ) : (
66
+ <Button variant={variant} size={size} className={className}>
67
+ {showFlag ? (
68
+ <LanguageFlag
69
+ code={locale}
70
+ rounded
71
+ className={cn('h-3 w-4 shrink-0', showTriggerLabel && 'mr-1.5')}
72
+ />
73
+ ) : showIcon ? (
74
+ <Globe className={cn('h-4 w-4 shrink-0', showTriggerLabel && 'mr-1')} />
75
+ ) : null}
76
+ {showTriggerLabel && <span>{currentLabel}</span>}
77
+ </Button>
78
+ )}
79
+ </DropdownMenuTrigger>
80
+ <DropdownMenuContent align="end">
81
+ {locales.map((code) => {
82
+ const meta = getLocaleMeta(code, labels);
83
+ const active = code === locale;
84
+ return (
85
+ <DropdownMenuItem
86
+ key={code}
87
+ onClick={() => onChange(code)}
88
+ className={cn(
89
+ 'flex items-center gap-2',
90
+ active && 'bg-accent',
91
+ )}
92
+ >
93
+ {showFlag && <LanguageFlag code={code} rounded className="h-3 w-4 shrink-0" />}
94
+ <span>{showCode ? `${code.toUpperCase()} - ${meta.native}` : meta.native}</span>
95
+ </DropdownMenuItem>
96
+ );
97
+ })}
98
+ </DropdownMenuContent>
99
+ </DropdownMenu>
100
+ );
101
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Globe } from 'lucide-react';
5
+
6
+ import { Button, LanguageFlag } from '@djangocfg/ui-core/components';
7
+ import { cn } from '@djangocfg/ui-core/lib';
8
+
9
+ import { getLocaleMeta } from './localeMeta';
10
+
11
+ export interface LocaleSwitcherTriggerProps {
12
+ locale: string;
13
+ labels?: Record<string, string>;
14
+ /** Show the flag in the trigger. @default true */
15
+ showFlag?: boolean;
16
+ /** Show the locale label / native name in the trigger. @default true */
17
+ showLabel?: boolean;
18
+ /** Show locale code instead of the native name (`'EN'` instead of `'English'`). */
19
+ showCode?: boolean;
20
+ variant?: 'ghost' | 'outline' | 'default';
21
+ size?: 'sm' | 'default' | 'lg' | 'icon';
22
+ className?: string;
23
+ ariaLabel?: string;
24
+ onClick?: () => void;
25
+ }
26
+
27
+ /**
28
+ * Footer / navbar trigger that opens the dialog. Visually matches the
29
+ * existing dropdown trigger so swapping `variant` does not shift the row.
30
+ */
31
+ export const LocaleSwitcherTrigger = React.forwardRef<
32
+ HTMLButtonElement,
33
+ LocaleSwitcherTriggerProps
34
+ >(function LocaleSwitcherTrigger(
35
+ {
36
+ locale,
37
+ labels,
38
+ showFlag = true,
39
+ showLabel = true,
40
+ showCode = false,
41
+ variant = 'outline',
42
+ size = 'default',
43
+ className,
44
+ ariaLabel,
45
+ onClick,
46
+ },
47
+ ref,
48
+ ) {
49
+ const meta = getLocaleMeta(locale, labels);
50
+ const labelText = showCode ? locale.toUpperCase() : meta.native;
51
+ const isIconOnly = size === 'icon';
52
+
53
+ // Icon-only trigger: full-bleed flag inside a circular pill that lines up
54
+ // with neighbouring avatars / icon buttons. No padding, no inner gap.
55
+ if (isIconOnly) {
56
+ return (
57
+ <Button
58
+ ref={ref}
59
+ type="button"
60
+ variant={variant}
61
+ size={size}
62
+ onClick={onClick}
63
+ aria-label={ariaLabel ?? labelText}
64
+ className={cn(
65
+ 'overflow-hidden rounded-full p-0',
66
+ className,
67
+ )}
68
+ >
69
+ {showFlag ? (
70
+ <LanguageFlag
71
+ code={locale}
72
+ className="h-full w-full object-cover"
73
+ />
74
+ ) : (
75
+ <Globe className="h-4 w-4" aria-hidden />
76
+ )}
77
+ </Button>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <Button
83
+ ref={ref}
84
+ type="button"
85
+ variant={variant}
86
+ size={size}
87
+ onClick={onClick}
88
+ aria-label={ariaLabel}
89
+ className={cn('inline-flex items-center gap-2', className)}
90
+ >
91
+ {showFlag ? (
92
+ <LanguageFlag
93
+ code={locale}
94
+ rounded
95
+ className="h-3 w-4 shrink-0"
96
+ />
97
+ ) : (
98
+ <Globe className="h-4 w-4 shrink-0" aria-hidden />
99
+ )}
100
+ {showLabel && <span className="truncate">{labelText}</span>}
101
+ </Button>
102
+ );
103
+ });
@@ -0,0 +1,27 @@
1
+ export { LocaleSwitcher } from './LocaleSwitcher';
2
+ export type { LocaleSwitcherProps } from './LocaleSwitcher';
3
+
4
+ export { LocaleSwitcherDialog } from './LocaleSwitcherDialog';
5
+ export type { LocaleSwitcherDialogProps } from './LocaleSwitcherDialog';
6
+
7
+ export { LocaleSwitcherDropdown } from './LocaleSwitcherDropdown';
8
+ export type { LocaleSwitcherDropdownProps } from './LocaleSwitcherDropdown';
9
+
10
+ export { LocaleSwitcherTrigger } from './LocaleSwitcherTrigger';
11
+ export type { LocaleSwitcherTriggerProps } from './LocaleSwitcherTrigger';
12
+
13
+ export { LocaleGrid } from './LocaleGrid';
14
+ export type { LocaleGridProps } from './LocaleGrid';
15
+
16
+ export { LocaleCard } from './LocaleCard';
17
+ export type { LocaleCardProps } from './LocaleCard';
18
+
19
+ export { getLocaleMeta } from './localeMeta';
20
+ export type { LocaleMeta } from './localeMeta';
21
+
22
+ export type {
23
+ LocaleSwitcherBrand,
24
+ LocaleSwitcherLabels,
25
+ LocaleSwitcherSharedProps,
26
+ LocaleSwitcherVariant,
27
+ } from './types';