@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.
- package/README.md +1 -1
- package/package.json +18 -18
- package/src/layouts/AppLayout/AppLayout.tsx +14 -11
- package/src/layouts/AppLayout/BaseApp.tsx +3 -1
- package/src/layouts/AppLayout/LayoutI18nProvider.tsx +59 -0
- package/src/layouts/AppLayout/index.ts +7 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -5
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +2 -4
- package/src/layouts/PublicLayout/README.md +13 -13
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +6 -11
- package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +15 -6
- package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +1 -5
- package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +1 -5
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -6
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +5 -9
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +2 -6
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +1 -7
- package/src/layouts/_components/LocaleSwitcher.tsx +40 -178
- package/src/layouts/_components/PrivateSidebarAccount.tsx +6 -8
- package/src/layouts/_components/UserMenu.tsx +15 -19
- package/src/layouts/_components/index.ts +23 -2
- package/src/layouts/_components/locale-switcher/LocaleCard.tsx +91 -0
- package/src/layouts/_components/locale-switcher/LocaleGrid.tsx +128 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +100 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherDialog.tsx +168 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +101 -0
- package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +103 -0
- package/src/layouts/_components/locale-switcher/index.ts +27 -0
- package/src/layouts/_components/locale-switcher/localeMeta.ts +109 -0
- package/src/layouts/_components/locale-switcher/types.ts +48 -0
- package/src/layouts/types/layout.types.ts +37 -1
|
@@ -7,12 +7,10 @@ import { Button } from '@djangocfg/ui-core/components';
|
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
|
|
9
9
|
|
|
10
|
-
import type { I18nLayoutConfig } from '../../types';
|
|
11
10
|
import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
|
|
11
|
+
import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
|
|
12
12
|
|
|
13
13
|
export interface NavControlsProps {
|
|
14
|
-
/** Optional i18n config. Required to render the locale switcher. */
|
|
15
|
-
i18n?: I18nLayoutConfig;
|
|
16
14
|
/** Show the theme (light / system / dark) pill. @default false */
|
|
17
15
|
showThemeSwitcher?: boolean;
|
|
18
16
|
/** Show the locale dropdown. Requires `i18n`. @default false */
|
|
@@ -81,12 +79,12 @@ function ThemeModeControl({ size }: { size: 'compact' | 'default' }) {
|
|
|
81
79
|
* to a navbar config.
|
|
82
80
|
*/
|
|
83
81
|
export function NavControls({
|
|
84
|
-
i18n,
|
|
85
82
|
showThemeSwitcher = false,
|
|
86
83
|
showLocaleSwitcher = false,
|
|
87
84
|
size = 'compact',
|
|
88
85
|
className,
|
|
89
86
|
}: NavControlsProps) {
|
|
87
|
+
const i18n = useLayoutI18nOptional();
|
|
90
88
|
const renderLocale = showLocaleSwitcher && Boolean(i18n);
|
|
91
89
|
if (!showThemeSwitcher && !renderLocale) return null;
|
|
92
90
|
|
|
@@ -98,12 +96,10 @@ export function NavControls({
|
|
|
98
96
|
return (
|
|
99
97
|
<div className={cn('inline-flex items-center gap-1.5', className)}>
|
|
100
98
|
{showThemeSwitcher && <ThemeModeControl size={size} />}
|
|
101
|
-
{renderLocale &&
|
|
99
|
+
{renderLocale && (
|
|
102
100
|
<LocaleSwitcher
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
onChange={i18n.onLocaleChange}
|
|
106
|
-
variant="outline"
|
|
101
|
+
variant="dialog"
|
|
102
|
+
buttonVariant="outline"
|
|
107
103
|
size={size === 'compact' ? 'sm' : 'default'}
|
|
108
104
|
showTriggerLabel={false}
|
|
109
105
|
className={localeBtnClass}
|
|
@@ -22,7 +22,7 @@ import { usePublicLayoutOptional } from '../context';
|
|
|
22
22
|
import { useMobileNavPanel } from '../hooks';
|
|
23
23
|
import { NavControls } from '../primitives/NavControls';
|
|
24
24
|
|
|
25
|
-
import type {
|
|
25
|
+
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
26
26
|
|
|
27
27
|
export interface MobileDrawerShellProps {
|
|
28
28
|
isOpen?: boolean;
|
|
@@ -38,8 +38,6 @@ export interface MobileDrawerShellProps {
|
|
|
38
38
|
showThemeSwitcher?: boolean;
|
|
39
39
|
showLocaleSwitcher?: boolean;
|
|
40
40
|
};
|
|
41
|
-
/** i18n config — required for the locale switcher row. */
|
|
42
|
-
i18n?: I18nLayoutConfig;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
@@ -81,8 +79,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
81
79
|
const hasSessionUser = Boolean(isAuthenticated && user);
|
|
82
80
|
const showSignInFooter = !hasSessionUser;
|
|
83
81
|
const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
|
|
84
|
-
const showLocaleSwitcher =
|
|
85
|
-
props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
|
|
82
|
+
const showLocaleSwitcher = props.controls?.showLocaleSwitcher === true;
|
|
86
83
|
const showControlsRow = showThemeSwitcher || showLocaleSwitcher;
|
|
87
84
|
|
|
88
85
|
return (
|
|
@@ -199,7 +196,6 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
|
|
|
199
196
|
{showControlsRow && (
|
|
200
197
|
<div className="shrink-0 border-t border-border/50 px-4 py-3 flex items-center justify-center gap-2">
|
|
201
198
|
<NavControls
|
|
202
|
-
i18n={props.i18n}
|
|
203
199
|
showThemeSwitcher={showThemeSwitcher}
|
|
204
200
|
showLocaleSwitcher={showLocaleSwitcher}
|
|
205
201
|
size="default"
|
|
@@ -50,8 +50,6 @@ import { NavBrand } from '../primitives/NavBrand';
|
|
|
50
50
|
import { NavControls } from '../primitives/NavControls';
|
|
51
51
|
import { NavDesktopItems } from '../primitives/NavDesktopItems';
|
|
52
52
|
|
|
53
|
-
import type { I18nLayoutConfig } from '../../types';
|
|
54
|
-
|
|
55
53
|
const heightCls: Record<PublicNavbarHeight, string> = {
|
|
56
54
|
sm: 'py-2',
|
|
57
55
|
md: 'py-3.5',
|
|
@@ -136,8 +134,6 @@ export interface NavbarShellProps {
|
|
|
136
134
|
actionsTrailingSlot?: ReactNode;
|
|
137
135
|
|
|
138
136
|
// ── Theme + locale controls (rendered next to UserMenu on desktop) ────────
|
|
139
|
-
/** i18n config — enables the locale switcher. Same type as `DefaultFooter`. */
|
|
140
|
-
i18n?: I18nLayoutConfig;
|
|
141
137
|
/** Toggle individual controls. Locale switcher also requires `i18n`. */
|
|
142
138
|
controls?: {
|
|
143
139
|
/** Light / system / dark pill. @default false */
|
|
@@ -267,12 +263,10 @@ export function NavbarShell(props: NavbarShellProps) {
|
|
|
267
263
|
) : null;
|
|
268
264
|
|
|
269
265
|
const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
|
|
270
|
-
const showLocaleSwitcher =
|
|
271
|
-
props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
|
|
266
|
+
const showLocaleSwitcher = props.controls?.showLocaleSwitcher === true;
|
|
272
267
|
const hasControls = showThemeSwitcher || showLocaleSwitcher;
|
|
273
268
|
const controlsNode = hasControls ? (
|
|
274
269
|
<NavControls
|
|
275
|
-
i18n={props.i18n}
|
|
276
270
|
showThemeSwitcher={showThemeSwitcher}
|
|
277
271
|
showLocaleSwitcher={showLocaleSwitcher}
|
|
278
272
|
/>
|
|
@@ -1,185 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Re-export shim — kept for backwards compatibility with code that imports
|
|
3
|
+
* `LocaleSwitcher` / `LOCALE_LABELS` from this path.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* For the "smart" version with next-intl hooks, use @djangocfg/nextjs/i18n/components
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* <LocaleSwitcher
|
|
10
|
-
* locale="en"
|
|
11
|
-
* locales={['en', 'ru', 'ko']}
|
|
12
|
-
* onChange={(locale) => router.push(`/${locale}`)}
|
|
13
|
-
* />
|
|
14
|
-
* ```
|
|
5
|
+
* The implementation now lives in `./locale-switcher/`.
|
|
15
6
|
*/
|
|
16
7
|
|
|
17
8
|
'use client';
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ar: 'العربية',
|
|
44
|
-
hi: 'हिन्दी',
|
|
45
|
-
tr: 'Türkçe',
|
|
46
|
-
pl: 'Polski',
|
|
47
|
-
nl: 'Nederlands',
|
|
48
|
-
uk: 'Українська',
|
|
49
|
-
|
|
50
|
-
// Regional variants
|
|
51
|
-
'pt-br': 'Português (Brasil)',
|
|
52
|
-
'pt-BR': 'Português (Brasil)',
|
|
53
|
-
'pt-pt': 'Português (Portugal)',
|
|
54
|
-
'pt-PT': 'Português (Portugal)',
|
|
55
|
-
'zh-cn': '简体中文',
|
|
56
|
-
'zh-CN': '简体中文',
|
|
57
|
-
'zh-tw': '繁體中文',
|
|
58
|
-
'zh-TW': '繁體中文',
|
|
59
|
-
'en-us': 'English (US)',
|
|
60
|
-
'en-US': 'English (US)',
|
|
61
|
-
'en-gb': 'English (UK)',
|
|
62
|
-
'en-GB': 'English (UK)',
|
|
63
|
-
'es-mx': 'Español (México)',
|
|
64
|
-
'es-MX': 'Español (México)',
|
|
65
|
-
'es-es': 'Español (España)',
|
|
66
|
-
'es-ES': 'Español (España)',
|
|
67
|
-
'fr-ca': 'Français (Canada)',
|
|
68
|
-
'fr-CA': 'Français (Canada)',
|
|
69
|
-
|
|
70
|
-
// Scandinavian languages
|
|
71
|
-
sv: 'Svenska',
|
|
72
|
-
no: 'Norsk',
|
|
73
|
-
nb: 'Norsk Bokmål',
|
|
74
|
-
nn: 'Norsk Nynorsk',
|
|
75
|
-
da: 'Dansk',
|
|
76
|
-
fi: 'Suomi',
|
|
77
|
-
is: 'Íslenska',
|
|
78
|
-
|
|
79
|
-
// Other European
|
|
80
|
-
cs: 'Čeština',
|
|
81
|
-
sk: 'Slovenčina',
|
|
82
|
-
hu: 'Magyar',
|
|
83
|
-
ro: 'Română',
|
|
84
|
-
bg: 'Български',
|
|
85
|
-
hr: 'Hrvatski',
|
|
86
|
-
sr: 'Српски',
|
|
87
|
-
sl: 'Slovenščina',
|
|
88
|
-
et: 'Eesti',
|
|
89
|
-
lv: 'Latviešu',
|
|
90
|
-
lt: 'Lietuvių',
|
|
91
|
-
el: 'Ελληνικά',
|
|
92
|
-
|
|
93
|
-
// Other
|
|
94
|
-
th: 'ไทย',
|
|
95
|
-
vi: 'Tiếng Việt',
|
|
96
|
-
id: 'Indonesia',
|
|
97
|
-
ms: 'Bahasa Melayu',
|
|
98
|
-
he: 'עברית',
|
|
99
|
-
fa: 'فارسی',
|
|
100
|
-
};
|
|
10
|
+
export {
|
|
11
|
+
LocaleSwitcher,
|
|
12
|
+
LocaleSwitcherDialog,
|
|
13
|
+
LocaleSwitcherDropdown,
|
|
14
|
+
LocaleSwitcherTrigger,
|
|
15
|
+
LocaleGrid,
|
|
16
|
+
LocaleCard,
|
|
17
|
+
getLocaleMeta,
|
|
18
|
+
} from './locale-switcher';
|
|
19
|
+
export type {
|
|
20
|
+
LocaleSwitcherProps,
|
|
21
|
+
LocaleSwitcherDialogProps,
|
|
22
|
+
LocaleSwitcherDropdownProps,
|
|
23
|
+
LocaleSwitcherTriggerProps,
|
|
24
|
+
LocaleGridProps,
|
|
25
|
+
LocaleCardProps,
|
|
26
|
+
LocaleMeta,
|
|
27
|
+
LocaleSwitcherBrand,
|
|
28
|
+
LocaleSwitcherLabels,
|
|
29
|
+
LocaleSwitcherSharedProps,
|
|
30
|
+
LocaleSwitcherVariant,
|
|
31
|
+
} from './locale-switcher';
|
|
32
|
+
|
|
33
|
+
import { getLocaleMeta as _getLocaleMeta } from './locale-switcher';
|
|
101
34
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
/** Button size */
|
|
116
|
-
size?: 'sm' | 'default' | 'lg' | 'icon';
|
|
117
|
-
/** Show icon (Globe icon, default: true) */
|
|
118
|
-
showIcon?: boolean;
|
|
119
|
-
/** Show flag emoji (default: true) */
|
|
120
|
-
showFlag?: boolean;
|
|
121
|
-
/** Show label text in trigger button (default: true) */
|
|
122
|
-
showTriggerLabel?: boolean;
|
|
123
|
-
/** Custom className */
|
|
124
|
-
className?: string;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function LocaleSwitcher({
|
|
128
|
-
locale,
|
|
129
|
-
locales,
|
|
130
|
-
onChange,
|
|
131
|
-
labels = {},
|
|
132
|
-
showCode = false,
|
|
133
|
-
variant = 'ghost',
|
|
134
|
-
size = 'sm',
|
|
135
|
-
showIcon = true,
|
|
136
|
-
showFlag = true,
|
|
137
|
-
showTriggerLabel = true,
|
|
138
|
-
className,
|
|
139
|
-
}: LocaleSwitcherProps) {
|
|
140
|
-
const allLabels = { ...LOCALE_LABELS, ...labels };
|
|
141
|
-
|
|
142
|
-
const getLabel = (code: string) => {
|
|
143
|
-
const label = allLabels[code] || code.toUpperCase();
|
|
144
|
-
if (showCode) {
|
|
145
|
-
return `${code.toUpperCase()} - ${label}`;
|
|
146
|
-
}
|
|
147
|
-
return label;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const getFlag = (code: string) => {
|
|
151
|
-
if (!showFlag) return null;
|
|
152
|
-
const flag = getLanguageFlag(code);
|
|
153
|
-
return flag || null;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const currentLabel = showCode ? locale.toUpperCase() : getLabel(locale);
|
|
157
|
-
const currentFlag = getFlag(locale);
|
|
158
|
-
|
|
159
|
-
return (
|
|
160
|
-
<DropdownMenu>
|
|
161
|
-
<DropdownMenuTrigger asChild>
|
|
162
|
-
<Button variant={variant} size={size} className={className}>
|
|
163
|
-
{showIcon && !currentFlag && <Globe className={showTriggerLabel ? "h-4 w-4 mr-1" : "h-4 w-4"} />}
|
|
164
|
-
{currentFlag && <span className={showTriggerLabel ? "mr-1" : ""}>{currentFlag}</span>}
|
|
165
|
-
{showTriggerLabel && <span>{currentLabel}</span>}
|
|
166
|
-
</Button>
|
|
167
|
-
</DropdownMenuTrigger>
|
|
168
|
-
<DropdownMenuContent align="end">
|
|
169
|
-
{locales.map((code) => {
|
|
170
|
-
const flag = getFlag(code);
|
|
171
|
-
return (
|
|
172
|
-
<DropdownMenuItem
|
|
173
|
-
key={code}
|
|
174
|
-
onClick={() => onChange(code)}
|
|
175
|
-
className={code === locale ? 'bg-accent' : ''}
|
|
176
|
-
>
|
|
177
|
-
{flag && <span className="mr-2">{flag}</span>}
|
|
178
|
-
{getLabel(code)}
|
|
179
|
-
</DropdownMenuItem>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
|
-
</DropdownMenuContent>
|
|
183
|
-
</DropdownMenu>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
35
|
+
/**
|
|
36
|
+
* Native-name labels for common locales. Built from the central locale-meta
|
|
37
|
+
* registry — kept as a `Record` for legacy callers that index it directly.
|
|
38
|
+
*/
|
|
39
|
+
export const LOCALE_LABELS: Record<string, string> = new Proxy(
|
|
40
|
+
{} as Record<string, string>,
|
|
41
|
+
{
|
|
42
|
+
get(_target, prop: string) {
|
|
43
|
+
if (typeof prop !== 'string') return undefined;
|
|
44
|
+
return _getLocaleMeta(prop).native;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -26,8 +26,8 @@ import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
|
|
|
26
26
|
|
|
27
27
|
import { useLogout } from '../../hooks';
|
|
28
28
|
import { LocaleSwitcher } from './LocaleSwitcher';
|
|
29
|
+
import { useLayoutI18nOptional } from '../AppLayout/LayoutI18nProvider';
|
|
29
30
|
|
|
30
|
-
import type { I18nLayoutConfig } from '../types';
|
|
31
31
|
import type { HeaderConfig } from '../PrivateLayout/PrivateLayout';
|
|
32
32
|
|
|
33
33
|
/** Radix portals (dropdown, select, popover) render outside the account node — ignore those clicks for “outside”. */
|
|
@@ -43,13 +43,13 @@ function isPointerFromRadixOverlay(target: EventTarget | null): boolean {
|
|
|
43
43
|
|
|
44
44
|
interface PrivateSidebarAccountProps {
|
|
45
45
|
header?: HeaderConfig;
|
|
46
|
-
i18n?: I18nLayoutConfig;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
export function PrivateSidebarAccount({ header
|
|
48
|
+
export function PrivateSidebarAccount({ header }: PrivateSidebarAccountProps) {
|
|
50
49
|
const { user } = useAuth();
|
|
51
50
|
const handleLogout = useLogout();
|
|
52
51
|
const t = useAppT();
|
|
52
|
+
const layoutI18n = useLayoutI18nOptional();
|
|
53
53
|
const { state, setOpen: setSidebarOpen } = useSidebar();
|
|
54
54
|
const [accountOpen, setAccountOpen] = React.useState(false);
|
|
55
55
|
const accountRootRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -150,12 +150,10 @@ export function PrivateSidebarAccount({ header, i18n }: PrivateSidebarAccountPro
|
|
|
150
150
|
</>
|
|
151
151
|
);
|
|
152
152
|
|
|
153
|
-
const localeThemeGroup =
|
|
153
|
+
const localeThemeGroup = layoutI18n ? (
|
|
154
154
|
<LocaleSwitcher
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
onChange={i18n.onLocaleChange}
|
|
158
|
-
variant="ghost"
|
|
155
|
+
variant="dropdown"
|
|
156
|
+
buttonVariant="ghost"
|
|
159
157
|
size="icon"
|
|
160
158
|
showTriggerLabel={false}
|
|
161
159
|
showIcon={false}
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
DropdownMenuSubContent,
|
|
51
51
|
DropdownMenuSubTrigger,
|
|
52
52
|
DropdownMenuTrigger,
|
|
53
|
-
|
|
53
|
+
LanguageFlag,
|
|
54
54
|
} from '@djangocfg/ui-core/components';
|
|
55
55
|
|
|
56
56
|
import { LOCALE_LABELS } from './LocaleSwitcher';
|
|
@@ -224,20 +224,19 @@ export function UserMenu({
|
|
|
224
224
|
</p>
|
|
225
225
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
226
226
|
{localeMenu.codes.map((code) => {
|
|
227
|
-
const flag = getLanguageFlag(code);
|
|
228
227
|
const active = code === localeMenu.current;
|
|
229
228
|
return (
|
|
230
229
|
<button
|
|
231
230
|
key={code}
|
|
232
231
|
type="button"
|
|
233
232
|
onClick={() => localeMenu.onChange(code)}
|
|
234
|
-
className={`rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors ${
|
|
233
|
+
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors ${
|
|
235
234
|
active
|
|
236
235
|
? 'border-primary bg-accent text-foreground'
|
|
237
236
|
: 'border-border/60 text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
238
237
|
}`}
|
|
239
238
|
>
|
|
240
|
-
{
|
|
239
|
+
<LanguageFlag code={code} className="h-3 w-4" rounded />
|
|
241
240
|
{localeLabel(code)}
|
|
242
241
|
</button>
|
|
243
242
|
);
|
|
@@ -333,21 +332,18 @@ export function UserMenu({
|
|
|
333
332
|
<span>{labels.language}</span>
|
|
334
333
|
</DropdownMenuSubTrigger>
|
|
335
334
|
<DropdownMenuSubContent>
|
|
336
|
-
{localeMenu.codes.map((code) =>
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
</DropdownMenuItem>
|
|
349
|
-
);
|
|
350
|
-
})}
|
|
335
|
+
{localeMenu.codes.map((code) => (
|
|
336
|
+
<DropdownMenuItem
|
|
337
|
+
key={code}
|
|
338
|
+
onSelect={() => {
|
|
339
|
+
localeMenu.onChange(code);
|
|
340
|
+
}}
|
|
341
|
+
className={code === localeMenu.current ? 'bg-accent' : ''}
|
|
342
|
+
>
|
|
343
|
+
<LanguageFlag code={code} className="mr-2 h-3 w-4" rounded />
|
|
344
|
+
{localeLabel(code)}
|
|
345
|
+
</DropdownMenuItem>
|
|
346
|
+
))}
|
|
351
347
|
</DropdownMenuSubContent>
|
|
352
348
|
</DropdownMenuSub>
|
|
353
349
|
</>
|
|
@@ -2,8 +2,29 @@
|
|
|
2
2
|
* Shared Layout Components
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export {
|
|
6
|
-
|
|
5
|
+
export {
|
|
6
|
+
LocaleSwitcher,
|
|
7
|
+
LocaleSwitcherDialog,
|
|
8
|
+
LocaleSwitcherDropdown,
|
|
9
|
+
LocaleSwitcherTrigger,
|
|
10
|
+
LocaleGrid,
|
|
11
|
+
LocaleCard,
|
|
12
|
+
getLocaleMeta,
|
|
13
|
+
LOCALE_LABELS,
|
|
14
|
+
} from './LocaleSwitcher';
|
|
15
|
+
export type {
|
|
16
|
+
LocaleSwitcherProps,
|
|
17
|
+
LocaleSwitcherDialogProps,
|
|
18
|
+
LocaleSwitcherDropdownProps,
|
|
19
|
+
LocaleSwitcherTriggerProps,
|
|
20
|
+
LocaleGridProps,
|
|
21
|
+
LocaleCardProps,
|
|
22
|
+
LocaleMeta,
|
|
23
|
+
LocaleSwitcherBrand,
|
|
24
|
+
LocaleSwitcherLabels,
|
|
25
|
+
LocaleSwitcherSharedProps,
|
|
26
|
+
LocaleSwitcherVariant,
|
|
27
|
+
} from './LocaleSwitcher';
|
|
7
28
|
|
|
8
29
|
export { UserMenu } from './UserMenu';
|
|
9
30
|
export type { UserMenuProps } from './UserMenu';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Check } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { LanguageFlag } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
import type { LocaleMeta } from './localeMeta';
|
|
10
|
+
|
|
11
|
+
export interface LocaleCardProps {
|
|
12
|
+
code: string;
|
|
13
|
+
meta: LocaleMeta;
|
|
14
|
+
active: boolean;
|
|
15
|
+
onSelect: (code: string) => void;
|
|
16
|
+
/** Compact list-row layout used on small viewports. @default false (grid tile) */
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One locale tile. Two flavours: spacious grid tile (desktop) and compact
|
|
22
|
+
* list row (mobile). Both share the same flag + native + english structure.
|
|
23
|
+
*/
|
|
24
|
+
export const LocaleCard = React.memo(function LocaleCard({
|
|
25
|
+
code,
|
|
26
|
+
meta,
|
|
27
|
+
active,
|
|
28
|
+
onSelect,
|
|
29
|
+
compact = false,
|
|
30
|
+
}: LocaleCardProps) {
|
|
31
|
+
const handleClick = React.useCallback(() => {
|
|
32
|
+
onSelect(code);
|
|
33
|
+
}, [code, onSelect]);
|
|
34
|
+
|
|
35
|
+
const baseClass = cn(
|
|
36
|
+
'group relative flex w-full cursor-pointer items-center gap-3 rounded-xl border text-left transition-colors',
|
|
37
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-1 focus-visible:ring-offset-background',
|
|
38
|
+
active
|
|
39
|
+
? 'border-primary bg-primary/[0.06] ring-1 ring-primary/25'
|
|
40
|
+
: 'border-border/60 bg-card hover:border-primary/40 hover:bg-accent/40',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (compact) {
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
aria-current={active ? 'true' : undefined}
|
|
49
|
+
className={cn(baseClass, 'min-h-[56px] px-3.5 py-2.5')}
|
|
50
|
+
>
|
|
51
|
+
<LanguageFlag
|
|
52
|
+
code={code}
|
|
53
|
+
rounded
|
|
54
|
+
className="h-5 w-7 shrink-0 shadow-[0_0_0_1px_rgb(0_0_0/0.06)]"
|
|
55
|
+
/>
|
|
56
|
+
<div className="min-w-0 flex-1">
|
|
57
|
+
<div className="truncate text-sm font-medium text-foreground">{meta.native}</div>
|
|
58
|
+
{meta.english && meta.english !== meta.native && (
|
|
59
|
+
<div className="truncate text-xs text-muted-foreground">{meta.english}</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
{active && <Check className="h-4 w-4 shrink-0 text-primary" aria-hidden />}
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={handleClick}
|
|
71
|
+
aria-current={active ? 'true' : undefined}
|
|
72
|
+
dir={meta.rtl ? 'rtl' : undefined}
|
|
73
|
+
className={cn(baseClass, 'min-h-[88px] flex-col items-start gap-2 px-4 py-3.5 sm:min-h-[96px]')}
|
|
74
|
+
>
|
|
75
|
+
<div className="flex w-full items-center justify-between">
|
|
76
|
+
<LanguageFlag
|
|
77
|
+
code={code}
|
|
78
|
+
rounded
|
|
79
|
+
className="h-6 w-9 shadow-[0_0_0_1px_rgb(0_0_0/0.08)]"
|
|
80
|
+
/>
|
|
81
|
+
{active && <Check className="h-4 w-4 text-primary" aria-hidden />}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<div className="truncate text-sm font-semibold text-foreground">{meta.native}</div>
|
|
85
|
+
{meta.english && meta.english !== meta.native && (
|
|
86
|
+
<div className="truncate text-xs text-muted-foreground">{meta.english}</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
});
|