@djangocfg/layouts 2.1.110 → 2.1.112
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 +69 -0
- package/package.json +14 -12
- package/src/components/errors/ErrorBoundary.tsx +12 -6
- package/src/components/errors/ErrorLayout.tsx +19 -9
- package/src/components/errors/errorConfig.ts +28 -22
- package/src/layouts/AppLayout/AppLayout.tsx +42 -17
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +8 -1
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +14 -1
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +128 -56
- package/src/layouts/PublicLayout/PublicLayout.tsx +6 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +20 -2
- package/src/layouts/_components/LocaleSwitcher.tsx +114 -0
- package/src/layouts/_components/UserMenu.tsx +14 -6
- package/src/layouts/_components/index.ts +3 -0
- package/src/layouts/index.ts +2 -2
- package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
- package/src/snippets/Breadcrumbs.tsx +19 -8
- package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
- package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocaleSwitcher Component (Presentational)
|
|
3
|
+
*
|
|
4
|
+
* "Dumb" locale switcher that receives data via props.
|
|
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
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use client';
|
|
18
|
+
|
|
19
|
+
import { Globe } from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Button,
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from '@djangocfg/ui-core/components';
|
|
28
|
+
|
|
29
|
+
// Default locale labels
|
|
30
|
+
const DEFAULT_LABELS: Record<string, string> = {
|
|
31
|
+
en: 'English',
|
|
32
|
+
ru: 'Русский',
|
|
33
|
+
ko: '한국어',
|
|
34
|
+
zh: '中文',
|
|
35
|
+
ja: '日本語',
|
|
36
|
+
es: 'Español',
|
|
37
|
+
fr: 'Français',
|
|
38
|
+
de: 'Deutsch',
|
|
39
|
+
pt: 'Português',
|
|
40
|
+
it: 'Italiano',
|
|
41
|
+
ar: 'العربية',
|
|
42
|
+
hi: 'हिन्दी',
|
|
43
|
+
tr: 'Türkçe',
|
|
44
|
+
pl: 'Polski',
|
|
45
|
+
nl: 'Nederlands',
|
|
46
|
+
uk: 'Українська',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface LocaleSwitcherProps {
|
|
50
|
+
/** Current locale */
|
|
51
|
+
locale: string;
|
|
52
|
+
/** Available locales */
|
|
53
|
+
locales: string[];
|
|
54
|
+
/** Callback when locale changes */
|
|
55
|
+
onChange: (locale: string) => void;
|
|
56
|
+
/** Custom labels for locales */
|
|
57
|
+
labels?: Record<string, string>;
|
|
58
|
+
/** Show locale code instead of/with label */
|
|
59
|
+
showCode?: boolean;
|
|
60
|
+
/** Button variant */
|
|
61
|
+
variant?: 'ghost' | 'outline' | 'default';
|
|
62
|
+
/** Button size */
|
|
63
|
+
size?: 'sm' | 'default' | 'lg' | 'icon';
|
|
64
|
+
/** Show icon */
|
|
65
|
+
showIcon?: boolean;
|
|
66
|
+
/** Custom className */
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function LocaleSwitcher({
|
|
71
|
+
locale,
|
|
72
|
+
locales,
|
|
73
|
+
onChange,
|
|
74
|
+
labels = {},
|
|
75
|
+
showCode = false,
|
|
76
|
+
variant = 'ghost',
|
|
77
|
+
size = 'sm',
|
|
78
|
+
showIcon = true,
|
|
79
|
+
className,
|
|
80
|
+
}: LocaleSwitcherProps) {
|
|
81
|
+
const allLabels = { ...DEFAULT_LABELS, ...labels };
|
|
82
|
+
|
|
83
|
+
const getLabel = (code: string) => {
|
|
84
|
+
const label = allLabels[code] || code.toUpperCase();
|
|
85
|
+
if (showCode) {
|
|
86
|
+
return `${code.toUpperCase()} - ${label}`;
|
|
87
|
+
}
|
|
88
|
+
return label;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const currentLabel = showCode ? locale.toUpperCase() : getLabel(locale);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<DropdownMenu>
|
|
95
|
+
<DropdownMenuTrigger asChild>
|
|
96
|
+
<Button variant={variant} size={size} className={className}>
|
|
97
|
+
{showIcon && <Globe className="h-4 w-4 mr-1" />}
|
|
98
|
+
<span>{currentLabel}</span>
|
|
99
|
+
</Button>
|
|
100
|
+
</DropdownMenuTrigger>
|
|
101
|
+
<DropdownMenuContent align="end">
|
|
102
|
+
{locales.map((code) => (
|
|
103
|
+
<DropdownMenuItem
|
|
104
|
+
key={code}
|
|
105
|
+
onClick={() => onChange(code)}
|
|
106
|
+
className={code === locale ? 'bg-accent' : ''}
|
|
107
|
+
>
|
|
108
|
+
{getLabel(code)}
|
|
109
|
+
</DropdownMenuItem>
|
|
110
|
+
))}
|
|
111
|
+
</DropdownMenuContent>
|
|
112
|
+
</DropdownMenu>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
import { LogOut } from 'lucide-react';
|
|
34
34
|
import Link from 'next/link';
|
|
35
|
-
import React from 'react';
|
|
35
|
+
import React, { useMemo } from 'react';
|
|
36
36
|
|
|
37
37
|
import { useAuth } from '@djangocfg/api/auth';
|
|
38
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
38
39
|
import {
|
|
39
40
|
Avatar, AvatarFallback, AvatarImage, Button, DropdownMenu, DropdownMenuContent,
|
|
40
41
|
DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,
|
|
@@ -59,6 +60,13 @@ export function UserMenu({
|
|
|
59
60
|
}: UserMenuProps) {
|
|
60
61
|
const { user, isAuthenticated, logout } = useAuth();
|
|
61
62
|
const [mounted, setMounted] = React.useState(false);
|
|
63
|
+
const t = useTypedT<I18nTranslations>();
|
|
64
|
+
|
|
65
|
+
const labels = useMemo(() => ({
|
|
66
|
+
signIn: t('layouts.profile.login'),
|
|
67
|
+
signOut: t('layouts.profile.signOut'),
|
|
68
|
+
userMenu: t('layouts.profile.userMenu'),
|
|
69
|
+
}), [t]);
|
|
62
70
|
|
|
63
71
|
React.useEffect(() => {
|
|
64
72
|
setMounted(true);
|
|
@@ -78,7 +86,7 @@ export function UserMenu({
|
|
|
78
86
|
allGroups.push({
|
|
79
87
|
items: [
|
|
80
88
|
{
|
|
81
|
-
label:
|
|
89
|
+
label: labels.signOut,
|
|
82
90
|
onClick: () => logout(),
|
|
83
91
|
icon: LogOut,
|
|
84
92
|
variant: 'destructive',
|
|
@@ -87,7 +95,7 @@ export function UserMenu({
|
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
return allGroups;
|
|
90
|
-
}, [groups, logout]);
|
|
98
|
+
}, [groups, logout, labels.signOut]);
|
|
91
99
|
|
|
92
100
|
if (!mounted) {
|
|
93
101
|
return null;
|
|
@@ -99,7 +107,7 @@ export function UserMenu({
|
|
|
99
107
|
return (
|
|
100
108
|
<Link href={authPath}>
|
|
101
109
|
<Button variant="default" className="w-full">
|
|
102
|
-
|
|
110
|
+
{labels.signIn}
|
|
103
111
|
</Button>
|
|
104
112
|
</Link>
|
|
105
113
|
);
|
|
@@ -107,7 +115,7 @@ export function UserMenu({
|
|
|
107
115
|
return (
|
|
108
116
|
<Link href={authPath}>
|
|
109
117
|
<Button variant="default" size="sm">
|
|
110
|
-
|
|
118
|
+
{labels.signIn}
|
|
111
119
|
</Button>
|
|
112
120
|
</Link>
|
|
113
121
|
);
|
|
@@ -194,7 +202,7 @@ export function UserMenu({
|
|
|
194
202
|
<AvatarImage src={userAvatar} alt={displayName} />
|
|
195
203
|
<AvatarFallback>{userInitial}</AvatarFallback>
|
|
196
204
|
</Avatar>
|
|
197
|
-
<span className="sr-only">
|
|
205
|
+
<span className="sr-only">{labels.userMenu}</span>
|
|
198
206
|
</Button>
|
|
199
207
|
</DropdownMenuTrigger>
|
|
200
208
|
<DropdownMenuContent align="end" className="w-56">
|
package/src/layouts/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
export * from './types';
|
|
12
12
|
|
|
13
13
|
// Shared components
|
|
14
|
-
export { UserMenu } from './_components';
|
|
15
|
-
export type { UserMenuProps } from './_components';
|
|
14
|
+
export { LocaleSwitcher, UserMenu } from './_components';
|
|
15
|
+
export type { LocaleSwitcherProps, UserMenuProps } from './_components';
|
|
16
16
|
|
|
17
17
|
// Smart layout router
|
|
18
18
|
export * from './AppLayout';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { LogIn } from 'lucide-react';
|
|
4
|
-
import React, { useState } from 'react';
|
|
4
|
+
import React, { useMemo, useState } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
8
|
import {
|
|
7
9
|
Button, Dialog, DialogContent, DialogHeader, DialogTitle
|
|
8
10
|
} from '@djangocfg/ui-core/components';
|
|
9
|
-
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
10
11
|
import { useEventListener } from '@djangocfg/ui-core';
|
|
11
12
|
|
|
12
13
|
// Re-export events for backwards compatibility
|
|
@@ -27,7 +28,15 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
27
28
|
authPath = '/auth'
|
|
28
29
|
}) => {
|
|
29
30
|
const [open, setOpen] = useState(false);
|
|
30
|
-
const
|
|
31
|
+
const t = useTypedT<I18nTranslations>();
|
|
32
|
+
|
|
33
|
+
const labels = useMemo(() => ({
|
|
34
|
+
authRequired: t('layouts.auth.authRequired'),
|
|
35
|
+
pleaseSignIn: t('layouts.auth.pleaseSignIn'),
|
|
36
|
+
goToSignIn: t('layouts.auth.goToSignIn'),
|
|
37
|
+
}), [t]);
|
|
38
|
+
|
|
39
|
+
const [message, setMessage] = useState<string>(labels.pleaseSignIn);
|
|
31
40
|
const router = useCfgRouter();
|
|
32
41
|
|
|
33
42
|
// Listen for open auth dialog event
|
|
@@ -44,7 +53,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
const handleClose = () => {
|
|
47
|
-
setMessage(
|
|
56
|
+
setMessage(labels.pleaseSignIn);
|
|
48
57
|
setOpen(false);
|
|
49
58
|
};
|
|
50
59
|
|
|
@@ -67,7 +76,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
67
76
|
<Dialog open={open} onOpenChange={handleClose}>
|
|
68
77
|
<DialogContent className="max-w-sm">
|
|
69
78
|
<DialogHeader>
|
|
70
|
-
<DialogTitle>
|
|
79
|
+
<DialogTitle>{labels.authRequired}</DialogTitle>
|
|
71
80
|
</DialogHeader>
|
|
72
81
|
|
|
73
82
|
<div className="space-y-4">
|
|
@@ -75,7 +84,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
75
84
|
|
|
76
85
|
<Button onClick={handleGoToAuth} className="w-full">
|
|
77
86
|
<LogIn className="h-4 w-4 mr-2" />
|
|
78
|
-
|
|
87
|
+
{labels.goToSignIn}
|
|
79
88
|
</Button>
|
|
80
89
|
</div>
|
|
81
90
|
</DialogContent>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { ChevronRight, Home } from 'lucide-react';
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import React from 'react';
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
8
|
|
|
7
9
|
export interface BreadcrumbItem {
|
|
8
10
|
path: string;
|
|
@@ -17,23 +19,32 @@ interface BreadcrumbsProps {
|
|
|
17
19
|
|
|
18
20
|
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
19
21
|
const pathname = usePathname();
|
|
20
|
-
|
|
22
|
+
const t = useTypedT<I18nTranslations>();
|
|
23
|
+
|
|
24
|
+
const labels = useMemo(() => ({
|
|
25
|
+
home: t('layouts.navigation.home'),
|
|
26
|
+
breadcrumb: t('layouts.navigation.breadcrumb'),
|
|
27
|
+
}), [t]);
|
|
28
|
+
|
|
21
29
|
// Use provided items or generate from pathname
|
|
22
|
-
const breadcrumbs =
|
|
30
|
+
const breadcrumbs = useMemo(() => {
|
|
31
|
+
if (items) return items;
|
|
32
|
+
return generateBreadcrumbsFromPath(pathname, labels.home);
|
|
33
|
+
}, [items, pathname, labels.home]);
|
|
23
34
|
|
|
24
35
|
if (breadcrumbs.length <= 1) {
|
|
25
36
|
return null;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
return (
|
|
29
|
-
<nav className={`flex items-center space-x-2 text-sm ${className}`} aria-label=
|
|
40
|
+
<nav className={`flex items-center space-x-2 text-sm ${className}`} aria-label={labels.breadcrumb}>
|
|
30
41
|
<ol className="flex items-center space-x-2">
|
|
31
42
|
{breadcrumbs.map((item: BreadcrumbItem, index: number) => (
|
|
32
43
|
<li key={item.path} className="flex items-center">
|
|
33
44
|
{index > 0 && (
|
|
34
45
|
<ChevronRight className="w-4 h-4 text-gray-400 mx-2" />
|
|
35
46
|
)}
|
|
36
|
-
|
|
47
|
+
|
|
37
48
|
{item.isActive ? (
|
|
38
49
|
<span className="text-gray-900 dark:text-white font-medium flex items-center gap-1">
|
|
39
50
|
{index === 0 && <Home className="w-4 h-4" />}
|
|
@@ -42,7 +53,7 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
|
42
53
|
) : (
|
|
43
54
|
<Link
|
|
44
55
|
href={item.path}
|
|
45
|
-
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200
|
|
56
|
+
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200
|
|
46
57
|
transition-colors duration-200 flex items-center gap-1 hover:underline"
|
|
47
58
|
>
|
|
48
59
|
{index === 0 && <Home className="w-4 h-4" />}
|
|
@@ -57,10 +68,10 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
|
57
68
|
};
|
|
58
69
|
|
|
59
70
|
// Helper function to generate breadcrumbs from pathname
|
|
60
|
-
function generateBreadcrumbsFromPath(pathname: string): BreadcrumbItem[] {
|
|
71
|
+
function generateBreadcrumbsFromPath(pathname: string, homeLabel: string = 'Home'): BreadcrumbItem[] {
|
|
61
72
|
const segments = pathname.split('/').filter(Boolean);
|
|
62
73
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
63
|
-
{ path: '/', label:
|
|
74
|
+
{ path: '/', label: homeLabel, isActive: pathname === '/' }
|
|
64
75
|
];
|
|
65
76
|
|
|
66
77
|
let currentPath = '';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Bot, PanelRight, RotateCcw, X } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { Button, Card, CardContent, CardFooter, CardHeader } from '@djangocfg/ui-core';
|
|
7
8
|
|
|
8
9
|
import { useAIChatContext } from '../context/AIChatContext';
|
|
@@ -21,6 +22,15 @@ export const ChatPanel = React.memo(() => {
|
|
|
21
22
|
stopStreaming,
|
|
22
23
|
clearMessages,
|
|
23
24
|
} = useAIChatContext();
|
|
25
|
+
const t = useTypedT<I18nTranslations>();
|
|
26
|
+
|
|
27
|
+
const labels = useMemo(() => ({
|
|
28
|
+
aiAssistant: t('layouts.chat.aiAssistant'),
|
|
29
|
+
defaultTitle: t('layouts.chat.defaultTitle'),
|
|
30
|
+
newChat: t('layouts.chat.newChat'),
|
|
31
|
+
closeChat: t('layouts.chat.closeChat'),
|
|
32
|
+
switchToSidebar: t('layouts.chat.switchToSidebar'),
|
|
33
|
+
}), [t]);
|
|
24
34
|
|
|
25
35
|
// Mobile: fullscreen, Desktop: floating panel
|
|
26
36
|
const panelStyles: React.CSSProperties = isMobile
|
|
@@ -60,8 +70,8 @@ export const ChatPanel = React.memo(() => {
|
|
|
60
70
|
<Bot className="h-4 w-4 text-primary" />
|
|
61
71
|
</div>
|
|
62
72
|
<div>
|
|
63
|
-
<h3 className="font-semibold text-sm">{config.title ||
|
|
64
|
-
<p className="text-xs text-muted-foreground">
|
|
73
|
+
<h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
|
|
74
|
+
<p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
|
|
65
75
|
</div>
|
|
66
76
|
</div>
|
|
67
77
|
<div className="flex gap-1">
|
|
@@ -71,7 +81,7 @@ export const ChatPanel = React.memo(() => {
|
|
|
71
81
|
size="icon"
|
|
72
82
|
className="h-8 w-8"
|
|
73
83
|
onClick={clearMessages}
|
|
74
|
-
title=
|
|
84
|
+
title={labels.newChat}
|
|
75
85
|
>
|
|
76
86
|
<RotateCcw className="h-4 w-4" />
|
|
77
87
|
</Button>
|
|
@@ -83,12 +93,12 @@ export const ChatPanel = React.memo(() => {
|
|
|
83
93
|
size="icon"
|
|
84
94
|
className="h-8 w-8"
|
|
85
95
|
onClick={() => setDisplayMode('sidebar')}
|
|
86
|
-
title=
|
|
96
|
+
title={labels.switchToSidebar}
|
|
87
97
|
>
|
|
88
98
|
<PanelRight className="h-4 w-4" />
|
|
89
99
|
</Button>
|
|
90
100
|
)}
|
|
91
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title=
|
|
101
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
|
|
92
102
|
<X className="h-4 w-4" />
|
|
93
103
|
</Button>
|
|
94
104
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Bot, GripVertical, PanelRightClose, RotateCcw, X } from 'lucide-react';
|
|
4
|
-
import React, { useEffect } from 'react';
|
|
4
|
+
import React, { useEffect, useMemo } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { Button } from '@djangocfg/ui-core';
|
|
7
8
|
|
|
8
9
|
import { useAIChatContext } from '../context/AIChatContext';
|
|
@@ -34,6 +35,17 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
34
35
|
stopStreaming,
|
|
35
36
|
clearMessages,
|
|
36
37
|
} = useAIChatContext();
|
|
38
|
+
const t = useTypedT<I18nTranslations>();
|
|
39
|
+
|
|
40
|
+
const labels = useMemo(() => ({
|
|
41
|
+
aiAssistant: t('layouts.chat.aiAssistant'),
|
|
42
|
+
defaultTitle: t('layouts.chat.defaultTitle'),
|
|
43
|
+
newChat: t('layouts.chat.newChat'),
|
|
44
|
+
closeChat: t('layouts.chat.closeChat'),
|
|
45
|
+
switchToFloating: t('layouts.chat.switchToFloating'),
|
|
46
|
+
dragToResize: t('layouts.chat.dragToResize'),
|
|
47
|
+
howCanIHelp: t('layouts.chat.howCanIHelp'),
|
|
48
|
+
}), [t]);
|
|
37
49
|
|
|
38
50
|
// Use the layout hook for content pushing and resizing
|
|
39
51
|
const { applyLayout, getSidebarStyles, startResize, isResizing } = useChatLayout();
|
|
@@ -65,7 +77,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
65
77
|
`}
|
|
66
78
|
style={{ width: resizeHandleWidth }}
|
|
67
79
|
onMouseDown={startResize}
|
|
68
|
-
title=
|
|
80
|
+
title={labels.dragToResize}
|
|
69
81
|
>
|
|
70
82
|
{showResizeIcon && (
|
|
71
83
|
<GripVertical className={`h-4 w-4 ${isResizing ? 'text-primary' : 'text-muted-foreground/50'}`} />
|
|
@@ -87,8 +99,8 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
87
99
|
<Bot className="h-4 w-4 text-primary" />
|
|
88
100
|
</div>
|
|
89
101
|
<div>
|
|
90
|
-
<h3 className="font-semibold text-sm">{config.title ||
|
|
91
|
-
<p className="text-xs text-muted-foreground">
|
|
102
|
+
<h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
|
|
103
|
+
<p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
|
|
92
104
|
</div>
|
|
93
105
|
</div>
|
|
94
106
|
<div className="flex gap-1">
|
|
@@ -98,7 +110,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
98
110
|
size="icon"
|
|
99
111
|
className="h-8 w-8"
|
|
100
112
|
onClick={clearMessages}
|
|
101
|
-
title=
|
|
113
|
+
title={labels.newChat}
|
|
102
114
|
>
|
|
103
115
|
<RotateCcw className="h-4 w-4" />
|
|
104
116
|
</Button>
|
|
@@ -108,11 +120,11 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
108
120
|
size="icon"
|
|
109
121
|
className="h-8 w-8"
|
|
110
122
|
onClick={() => setDisplayMode('floating')}
|
|
111
|
-
title=
|
|
123
|
+
title={labels.switchToFloating}
|
|
112
124
|
>
|
|
113
125
|
<PanelRightClose className="h-4 w-4" />
|
|
114
126
|
</Button>
|
|
115
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title=
|
|
127
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
|
|
116
128
|
<X className="h-4 w-4" />
|
|
117
129
|
</Button>
|
|
118
130
|
</div>
|
|
@@ -128,7 +140,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
128
140
|
isCompact={false}
|
|
129
141
|
largeGreetingIcon
|
|
130
142
|
greetingIcon="message"
|
|
131
|
-
greetingTitle=
|
|
143
|
+
greetingTitle={labels.howCanIHelp}
|
|
132
144
|
/>
|
|
133
145
|
</div>
|
|
134
146
|
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { ChevronRight, Download, Share, X } from 'lucide-react';
|
|
15
|
-
import React, { useEffect, useState } from 'react';
|
|
15
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
16
16
|
|
|
17
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
17
18
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
19
|
|
|
19
20
|
import { useInstall } from '../context/InstallContext';
|
|
@@ -69,6 +70,18 @@ export function A2HSHint({
|
|
|
69
70
|
const [show, setShow] = useState(false);
|
|
70
71
|
const [showGuide, setShowGuide] = useState(false);
|
|
71
72
|
const [installing, setInstalling] = useState(false);
|
|
73
|
+
const t = useTypedT<I18nTranslations>();
|
|
74
|
+
|
|
75
|
+
const labels = useMemo(() => ({
|
|
76
|
+
addToHomeScreen: t('layouts.pwa.addToHomeScreen'),
|
|
77
|
+
installApp: t('layouts.pwa.installApp'),
|
|
78
|
+
tapToLearnHow: t('layouts.pwa.tapToLearnHow'),
|
|
79
|
+
clickToSeeGuide: t('layouts.pwa.clickToSeeGuide'),
|
|
80
|
+
tapToInstall: t('layouts.pwa.tapToInstall'),
|
|
81
|
+
installing: t('layouts.pwa.installing'),
|
|
82
|
+
dismiss: t('layouts.pwa.dismiss'),
|
|
83
|
+
appLogo: t('layouts.pwa.appLogo'),
|
|
84
|
+
}), [t]);
|
|
72
85
|
|
|
73
86
|
// Determine if should show hint
|
|
74
87
|
// Production: iOS (all browsers support PWA via Share) & Android Chrome with native prompt
|
|
@@ -141,15 +154,15 @@ export function A2HSHint({
|
|
|
141
154
|
let subtitle: React.ReactNode;
|
|
142
155
|
|
|
143
156
|
if (isIOS) {
|
|
144
|
-
title =
|
|
145
|
-
subtitle = <>
|
|
157
|
+
title = labels.addToHomeScreen;
|
|
158
|
+
subtitle = <>{labels.tapToLearnHow} <ChevronRight className="w-3 h-3" /></>;
|
|
146
159
|
} else if (isDesktop) {
|
|
147
|
-
title =
|
|
148
|
-
subtitle = <>
|
|
160
|
+
title = labels.installApp;
|
|
161
|
+
subtitle = <>{labels.clickToSeeGuide} <ChevronRight className="w-3 h-3" /></>;
|
|
149
162
|
} else {
|
|
150
163
|
// Android or other mobile with native prompt
|
|
151
|
-
title =
|
|
152
|
-
subtitle = <>
|
|
164
|
+
title = labels.installApp;
|
|
165
|
+
subtitle = <>{labels.tapToInstall} <Download className="w-3 h-3" /></>;
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
return (
|
|
@@ -180,7 +193,7 @@ export function A2HSHint({
|
|
|
180
193
|
{/* App logo or icon */}
|
|
181
194
|
<div className="flex-shrink-0">
|
|
182
195
|
{logo ? (
|
|
183
|
-
<img src={logo} alt=
|
|
196
|
+
<img src={logo} alt={labels.appLogo} className="w-10 h-10 rounded-lg" />
|
|
184
197
|
) : (
|
|
185
198
|
<Share className="w-5 h-5 text-blue-400" />
|
|
186
199
|
)}
|
|
@@ -190,7 +203,7 @@ export function A2HSHint({
|
|
|
190
203
|
<div className="flex-1 min-w-0">
|
|
191
204
|
<p className="text-sm font-medium text-white mb-1">{title}</p>
|
|
192
205
|
<p className="text-xs text-zinc-400 flex items-center gap-1">
|
|
193
|
-
{installing ?
|
|
206
|
+
{installing ? labels.installing : subtitle}
|
|
194
207
|
</p>
|
|
195
208
|
</div>
|
|
196
209
|
|
|
@@ -201,7 +214,7 @@ export function A2HSHint({
|
|
|
201
214
|
handleDismiss();
|
|
202
215
|
}}
|
|
203
216
|
className="flex-shrink-0 p-1 hover:bg-zinc-700 rounded transition-colors"
|
|
204
|
-
aria-label=
|
|
217
|
+
aria-label={labels.dismiss}
|
|
205
218
|
>
|
|
206
219
|
<X className="w-4 h-4 text-zinc-400" />
|
|
207
220
|
</button>
|