Files
zeekay cb6db9e659 design: every page and component outside the landing and beta paths is at zero lint
Inline style objects became gui props (Box for a div, View or Text rendering the
element for the rest, since Box styles only a div), raw colours and sizes became
the design's tokens, the brand swatches read @hanzo/design's own values, and the
project and creation hues moved to palette.json, which is data. The newsletter's
subscribe button is the standard filled Action. connect-steps.tsx, which nothing
imports, is deleted with the brand constants nobody read.

Pages compared before and after at 1440x900 and 390x844: blockchain, brand,
directory, projects, settings, unsubscribe identical; founder 0.03% of pixels;
buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's
button is now filled.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:49:42 -07:00

255 lines
8.8 KiB
TypeScript

'use client'
import { useState } from 'react'
import {
Check,
CreditCard,
ExternalLink,
HelpCircle,
Languages,
LogOut,
Search,
Settings,
Sparkles,
User,
Users,
} from 'lucide-react'
import {
Avatar,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
Input,
Text,
View,
XStack,
YStack,
} from '@hanzo/ui'
import { rememberDestination } from '@hanzo/app/lib/destination'
import { locales } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
export interface AccountAdminWidgetProps {
person: {
name?: string
email?: string
avatar?: string
}
onSignOut: () => void | Promise<void>
}
export function AccountAdminWidget({ person, onSignOut }: AccountAdminWidgetProps) {
const { t, dir, locale, setLocale } = useI18n()
const [langOpen, setLangOpen] = useState(false)
const [langQuery, setLangQuery] = useState('')
const name = person.name || person.email || t('Account')
const email = person.email || ''
const avatar = person.avatar || null
const q = langQuery.trim().toLowerCase()
const filteredLocales = locales
.filter((l) => !l.review || l.code === locale)
.filter((l) => !q || l.name.toLowerCase().includes(q) || l.englishName.toLowerCase().includes(q) || l.code.toLowerCase().includes(q))
const handleUseAnotherAccount = () => {
rememberDestination()
void onSignOut()
}
const handleLogOut = () => {
void onSignOut()
}
return (
<XStack render="section" aria-label={t('Account')} items="center">
<DropdownMenu placement={dir === 'rtl' ? 'bottom-start' : 'bottom-end'} offset={8}>
<DropdownMenuTrigger asChild>
<XStack
render={<button type="button" />}
aria-label={t('Account: {name}', { name })}
items="center"
gap={8}
p={2}
pr={8}
rounded={999}
borderWidth={1}
borderColor="var(--border)"
bg="transparent"
hoverStyle={{ borderColor: 'var(--foreground)', bg: '$hover' }}
cursor="pointer"
$platform-web={{ transition: 'border-color 160ms, background-color 160ms' }}
>
<Avatar src={avatar} name={name} alt="" width={30} height={30} rounded={999} />
<Text
fontSize="$2"
fontWeight="500"
color="var(--foreground)"
numberOfLines={1}
maxW={120}
display="none"
$md={{ display: 'flex' }}
>
{name.split(' ')[0]}
</Text>
</XStack>
</DropdownMenuTrigger>
<DropdownMenuContent minW={240} maxW="calc(100vw - 16px)" aria-label={t('Account')}>
<DropdownMenuLabel>
<XStack items="center" gap={10} py={4}>
<Avatar src={avatar} name={name} alt="" width={36} height={36} rounded={999} />
<YStack minW={0} gap={2} flex={1}>
<Text fontSize="$3" fontWeight="600" color="$ink" numberOfLines={1}>
{name}
</Text>
{email ? (
<Text fontSize="$1" color="$soft" numberOfLines={1}>
{email}
</Text>
) : null}
</YStack>
</XStack>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => window.location.assign('/settings')}>
<XStack items="center" gap={8} width="100%">
<Settings size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Settings & Profile')}
</Text>
</XStack>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => window.location.assign('/beta')}>
<XStack items="center" gap={8} width="100%">
<Sparkles size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Beta Dashboard')}
</Text>
</XStack>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => window.location.assign('/billing')}>
<XStack items="center" gap={8} width="100%">
<CreditCard size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Billing & Plans')}
</Text>
</XStack>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => window.location.assign('/console')}>
<XStack items="center" justify="space-between" width="100%">
<XStack items="center" gap={8}>
<User size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Organization & Console')}
</Text>
</XStack>
<ExternalLink size={12} color="var(--muted-foreground)" aria-hidden />
</XStack>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub
open={langOpen}
onOpenChange={(next) => {
setLangOpen(next)
if (!next) setLangQuery('')
}}
>
<DropdownMenuSubTrigger>
<XStack items="center" gap={8} width="100%">
<Languages size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Language')}
</Text>
</XStack>
</DropdownMenuSubTrigger>
{langOpen ? (
<DropdownMenuSubContent minW={240} maxW="calc(100vw - 16px)" pointerEvents="auto">
<View px={6} py={4} onKeyDown={(e) => e.stopPropagation()}>
<XStack
items="center"
gap={6}
px={8}
height={30}
rounded={8}
borderWidth={1}
borderColor="var(--border)"
bg="transparent"
>
<Search size={13} color="var(--muted-foreground)" aria-hidden />
<Input
unstyled
value={langQuery}
onChangeText={setLangQuery}
placeholder={t('Search language...')}
aria-label={t('Search language...')}
fontSize="$2"
color="$ink"
placeholderTextColor="$soft"
flex={1}
minW={0}
height={28}
/>
</XStack>
</View>
<YStack maxH={260} overflowY="auto">
{filteredLocales.map((l) => (
<DropdownMenuItem key={l.code} onSelect={() => void setLocale(l.code)}>
<XStack items="center" justify="space-between" width="100%" gap={8}>
<Text fontSize="$2" color="$ink" numberOfLines={1} flex={1}>
{l.englishName !== l.name ? `${l.name} (${l.englishName})` : l.name}
</Text>
{l.code === locale ? <Check size={15} color="var(--foreground)" aria-label={t('Current')} /> : null}
</XStack>
</DropdownMenuItem>
))}
</YStack>
</DropdownMenuSubContent>
) : null}
</DropdownMenuSub>
<DropdownMenuItem onSelect={() => window.location.assign('/support')}>
<XStack items="center" gap={8} width="100%">
<HelpCircle size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Help & Support')}
</Text>
</XStack>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleUseAnotherAccount}>
<XStack items="center" gap={8} width="100%">
<Users size={15} color="var(--muted-foreground)" aria-hidden />
<Text fontSize="$2" color="$ink">
{t('Use another account')}
</Text>
</XStack>
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleLogOut}>
<XStack items="center" gap={8} width="100%">
<LogOut size={15} color="var(--destructive)" aria-hidden />
<Text fontSize="$2" color="var(--destructive)">
{t('Log out')}
</Text>
</XStack>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</XStack>
)
}