// From DeskPark apps/web/src/components/Seat.tsx and lib/media.ts (MIT, see viewer/NOTICE), cut down for a viewer: the // pixel avatars and figures become an initials circle (Shoal's avatars go here later), and the countdown, AI wake // button and speech go, since the room protocol carries none of them. import type { ReactNode } from 'react'; import { useSyncExternalStore } from 'react'; export interface PlayerInfo { name: string; /** a bot or auto plays this seat */ isBot?: boolean; isMe?: boolean; } function initials(name: string): string { const words = name.trim().split(/[\s_\-.]+/).filter(Boolean); if (!words.length) return '?'; if (!/^[\x20-\x7e]/.test(words[0])) return [...words[0]][0]; // a Chinese name: its first character return (words.length > 1 ? words[0][0] + words[1][0] : words[0].slice(0, 2)).toUpperCase(); } export function Avatar({ name, size = 44 }: { name: string; size?: number }) { let h = 7; for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360; return ( ); } const COMPACT_QUERY = '(max-height: 500px)'; /** 横屏手机等矮屏幕 */ function useCompact(): boolean { return useSyncExternalStore( (cb) => { const m = window.matchMedia(COMPACT_QUERY); m.addEventListener('change', cb); return () => m.removeEventListener('change', cb); }, () => window.matchMedia(COMPACT_QUERY).matches, ); } /** 大屏幕上的座位放大倍数:1440×860 以下为 1,最大 1.5 */ function seatScale(): number { const s = Math.min(window.innerWidth / 1440, window.innerHeight / 860); return Math.round(Math.min(1.5, Math.max(1, s)) * 20) / 20; } function useSeatScale(): number { return useSyncExternalStore((cb) => { window.addEventListener('resize', cb); return () => window.removeEventListener('resize', cb); }, seatScale); } export interface SeatProps { player: PlayerInfo; active?: boolean; /** 头像右上角的身份标签,例如"地主" */ tag?: ReactNode; tagTone?: 'gold' | 'plain' | 'accent'; /** 名字下方的副信息,例如剩余张数 */ sub?: ReactNode; /** 状态气泡,例如"叫地主" */ bubble?: ReactNode; bubbleSide?: 'top' | 'bottom' | 'left' | 'right'; size?: number; layout?: 'column' | 'row'; children?: ReactNode; } export function Seat({ player, active, tag, tagTone = 'gold', sub, bubble, bubbleSide = 'top', size = 60, layout = 'column', children, }: SeatProps) { const compact = useCompact(); const scale = useSeatScale(); // 横屏手机等矮屏幕上自动缩小,大屏幕上适当放大 const px = compact ? Math.round(size * 0.7) : Math.round(size * scale); return (
{tag && {tag}} {player.isBot && ( )}
{player.name} {player.isMe && 我} {sub !== undefined && {sub}}
{bubble &&
{bubble}
} {children}
); }