diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx
index 12ef4e5..3a50883 100644
--- a/src/components/LanguageToggle.tsx
+++ b/src/components/LanguageToggle.tsx
@@ -7,47 +7,10 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { useTranslation, type Language } from '@/contexts/language';
+import { useTranslation } from '@/contexts/language';
+import { LANGUAGE_OPTIONS } from '@/lib/languageOptions';
import { cn } from '@/lib/utils';
-interface LanguageOption {
- code: Language;
- name: string;
- nativeName: string;
- flag: string;
-}
-
-const languages: LanguageOption[] = [
- { code: 'en', name: 'English', nativeName: 'English', flag: '/flags/gb-eng.svg' },
- { code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
- { code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '/flags/ca.svg' },
- { code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '/flags/eu.svg' },
- { code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '/flags/gl.svg' },
- { code: 'pt', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷' },
- { code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', flag: '🇵🇹' },
- { code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
- { code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
- { code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
- { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
- { code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
- { code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
- { code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
- { code: 'ko-KP', name: 'Korean (North)', nativeName: '조선어', flag: '🇰🇵' },
- { code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
- { code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
- { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳' },
- { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼' },
- { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦' },
- { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩' },
- { code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿' },
- { code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴' },
- { code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪' },
- { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
- { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
- { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
- { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
-];
-
interface LanguageToggleProps {
/** Show only the flag icon (compact mode) */
compact?: boolean;
@@ -93,17 +56,23 @@ export function LanguageToggle({ compact = false, inline = false, className, var
const { language, setLanguage, t } = useTranslation();
const [query, setQuery] = useState('');
+ // Auto-focusing the search on a phone pops the keyboard over the list before
+ // the user can see the languages. Keep it for mouse/pen, not touch.
+ const autoFocusSearch =
+ typeof window !== 'undefined' && !!window.matchMedia?.('(pointer: fine)').matches;
+
const safeQuery = query.trim().toLowerCase();
const filteredLanguages = safeQuery
- ? languages.filter(
+ ? LANGUAGE_OPTIONS.filter(
(l) =>
l.name.toLowerCase().includes(safeQuery) ||
l.nativeName.toLowerCase().includes(safeQuery) ||
l.code.toLowerCase().includes(safeQuery)
)
- : languages;
+ : LANGUAGE_OPTIONS;
- const currentLanguage = languages.find(l => l.code === language) || languages[0];
+ const currentLanguage =
+ LANGUAGE_OPTIONS.find((l) => l.code === language) || LANGUAGE_OPTIONS[0];
if (inline) {
return (
@@ -177,7 +146,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
onPointerDown={(e) => e.stopPropagation()}
>
setQuery(e.target.value)}
placeholder={t('nav.searchLanguages')}
@@ -189,7 +158,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
key={lang.code}
onClick={() => setLanguage(lang.code)}
className={cn(
- 'flex items-center gap-3 cursor-pointer',
+ 'flex min-h-11 items-center gap-3 cursor-pointer',
language === lang.code && 'bg-primary/10 text-primary'
)}
>
@@ -225,7 +194,7 @@ export function LanguageSelector() {
{t('settings.selectLanguage')}
- {languages.map((lang) => (
+ {LANGUAGE_OPTIONS.map((lang) => (
setLanguage(lang.code)}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 8dd0c64..141e56e 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -16,6 +16,7 @@ import {
import { LoginArea } from '@/components/auth/LoginArea';
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
import { LanguageToggle } from '@/components/LanguageToggle';
+import { LANGUAGE_OPTIONS } from '@/lib/languageOptions';
import { clearTranslationMemory } from '@/lib/translation';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTranslation } from '@/contexts/language';
@@ -46,6 +47,9 @@ import {
SlidersHorizontal,
BookOpen,
PersonStanding,
+ Languages,
+ ChevronLeft,
+ ChevronRight,
} from 'lucide-react';
import { OfflineIndicator } from '@/components/OfflineIndicator';
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
@@ -150,14 +154,14 @@ function NavLink({
to={href}
onClick={onClick}
className={cn(
- 'flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200',
+ 'flex min-w-0 items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-200',
isActive
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/25'
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
)}
>
-
- {label}
+
+ {label}
);
}
@@ -203,7 +207,7 @@ function MobileHeaderUserMenu({
-
+
{/* User info header */}
@@ -513,6 +517,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
// padding even though it shares this one persistent layout with every route.
const isFullBleed = noPadding || location.pathname === '/' || location.pathname === '/map';
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [mobileLanguageView, setMobileLanguageView] = useState(false);
const [showStatusDialog, setShowStatusDialog] = useState(false);
const [showCustomizeNav, setShowCustomizeNav] = useState(false);
const [showAddAccount, setShowAddAccount] = useState(false);
@@ -539,7 +544,8 @@ export function Layout({ children, noPadding }: LayoutProps) {
const timer = window.setTimeout(() => setShowWelcomeLogin(true), 900);
return () => window.clearTimeout(timer);
}, [user]);
- const { t } = useTranslation();
+ const { t, language } = useTranslation();
+ const currentLanguage = LANGUAGE_OPTIONS.find((item) => item.code === language) ?? LANGUAGE_OPTIONS[0];
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
const author = useAuthor(user?.pubkey || '');
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
@@ -624,7 +630,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
{t('nav.more')}
-
+
{/* Catalog items the user hasn't pinned to the top bar.
Auth-only items (stash/collections/contacts/wallet/messages)
only show when logged in. */}
@@ -822,7 +828,13 @@ export function Layout({ children, noPadding }: LayoutProps) {
-
+ {
+ setMobileMenuOpen(open);
+ if (!open) setMobileLanguageView(false);
+ }}
+ >
@@ -830,7 +842,43 @@ export function Layout({ children, noPadding }: LayoutProps) {
{t('chromeUi.menu')}
+ {mobileLanguageView ? (
+
+
setMobileLanguageView(false)}
+ className="flex items-center gap-2 self-start rounded-lg px-1 py-1 text-sm font-bold text-muted-foreground transition-colors hover:text-foreground"
+ >
+ {t('common.back')}
+
+
+
+ {t('settings.selectLanguage')}
+
+
+
+
+ ) : (
+ {/* Language picker lives in its own view — 28 languages inline
+ made this sheet ~3,200px tall and buried the control. */}
+ setMobileLanguageView(true)}
+ className="flex w-full items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10 active:scale-[0.99]"
+ >
+
+
+
+ {t('settings.selectLanguage')}
+
+
+ {currentLanguage.nativeName}
+
+
+
+
+
{/* Wallet Card - Mobile Menu */}
{user && (
{t('chromeUi.customizeNavBar')}
- {/* Language Toggle in Mobile Menu — inline (a portaled dropdown
- is unclickable inside a Radix Sheet) */}
-
-
-
{/* Login/Logout Section */}
{user ? (
@@ -1063,6 +1106,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
)}
+ )}
diff --git a/src/components/crew/CordnNativePanel.tsx b/src/components/crew/CordnNativePanel.tsx
index 3d31fb6..29b88b1 100644
--- a/src/components/crew/CordnNativePanel.tsx
+++ b/src/components/crew/CordnNativePanel.tsx
@@ -5,6 +5,7 @@ import {
Check,
ChevronDown,
Copy,
+ ExternalLink,
ImagePlus,
KeyRound,
Loader2,
@@ -780,25 +781,36 @@ function AdvancedPanel({
{groupRef && (
-
+ {' '}
+ {t('socialUi.crewCoordinator.openCordnGroup')}
+
+ >
)}
diff --git a/src/lib/languageOptions.ts b/src/lib/languageOptions.ts
new file mode 100644
index 0000000..933be53
--- /dev/null
+++ b/src/lib/languageOptions.ts
@@ -0,0 +1,43 @@
+import type { Language } from '@/contexts/language';
+
+export interface LanguageOption {
+ code: Language;
+ name: string;
+ nativeName: string;
+ flag: string;
+}
+
+/**
+ * Every locale the picker can show. Kept out of the component file so Fast
+ * Refresh still works (a component module must only export components).
+ */
+export const LANGUAGE_OPTIONS: LanguageOption[] = [
+ { code: 'en', name: 'English', nativeName: 'English', flag: '/flags/gb-eng.svg' },
+ { code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
+ { code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '/flags/ca.svg' },
+ { code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '/flags/eu.svg' },
+ { code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '/flags/gl.svg' },
+ { code: 'pt', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷' },
+ { code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', flag: '🇵🇹' },
+ { code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
+ { code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
+ { code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
+ { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
+ { code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
+ { code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
+ { code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
+ { code: 'ko-KP', name: 'Korean (North)', nativeName: '조선어', flag: '🇰🇵' },
+ { code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
+ { code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
+ { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳' },
+ { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼' },
+ { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦' },
+ { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩' },
+ { code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿' },
+ { code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴' },
+ { code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪' },
+ { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
+ { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
+ { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
+ { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
+];
diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx
index f5fb44b..072f94a 100644
--- a/src/pages/CrewDetailPage.tsx
+++ b/src/pages/CrewDetailPage.tsx
@@ -347,18 +347,21 @@ export default function CrewDetailPage() {
}}
className="w-full"
>
-
-
- {t('socialUi.crew.eventsTab')}
+
+
+ {' '}
+ {t('socialUi.crew.eventsTab')}
{crewEvents && crewEvents.length > 0 && (
- {crewEvents.length}
+ {crewEvents.length}
)}
-
- {t('socialUi.crew.chatTab')}
+
+ {' '}
+ {t('socialUi.crew.chatTab')}
-
- {t('cordnUi.title')}
+
+ {' '}
+ {t('cordnUi.title')}