/** * Mandu Kitchen DevTools - Islands Panel * @version 1.0.3 */ import React, { useMemo } from 'react'; import type { IslandSnapshot } from '../../../types'; import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens'; // ============================================================================ // Styles // ============================================================================ const styles = { container: { padding: spacing.md, height: '100%', display: 'flex', flexDirection: 'column' as const, gap: spacing.md, }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', }, stats: { display: 'flex', gap: spacing.md, }, stat: { display: 'flex', alignItems: 'center', gap: spacing.xs, fontSize: typography.fontSize.xs, color: colors.text.secondary, }, statValue: { fontWeight: typography.fontWeight.semibold, color: colors.text.primary, }, timeline: { flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column' as const, gap: spacing.xs, }, emptyState: { flex: 1, display: 'flex', flexDirection: 'column' as const, alignItems: 'center', justifyContent: 'center', gap: spacing.md, padding: spacing.xl, color: colors.text.muted, fontSize: typography.fontSize.sm, textAlign: 'center' as const, }, islandItem: { display: 'flex', alignItems: 'center', gap: spacing.md, padding: spacing.sm, borderRadius: borderRadius.md, backgroundColor: colors.background.medium, transition: `all ${animation.duration.fast}`, }, islandIcon: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '48px', height: '28px', padding: '0 8px', borderRadius: borderRadius.full, textAlign: 'center' as const, fontSize: typography.fontSize.xs, fontWeight: typography.fontWeight.bold, fontFamily: typography.fontFamily.mono, letterSpacing: '0.08em', }, islandInfo: { flex: 1, minWidth: 0, }, islandName: { fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.medium, color: colors.text.primary, whiteSpace: 'nowrap' as const, overflow: 'hidden', textOverflow: 'ellipsis', }, islandMeta: { display: 'flex', alignItems: 'center', gap: spacing.sm, marginTop: '2px', }, badge: { padding: `1px ${spacing.xs}`, borderRadius: borderRadius.sm, fontSize: typography.fontSize.xs, fontWeight: typography.fontWeight.medium, }, timeBar: { width: '80px', height: '4px', borderRadius: borderRadius.full, backgroundColor: colors.background.light, overflow: 'hidden', }, timeBarFill: { height: '100%', borderRadius: borderRadius.full, transition: `width ${animation.duration.normal}`, }, timing: { fontSize: typography.fontSize.xs, color: colors.text.muted, fontFamily: typography.fontFamily.mono, minWidth: '50px', textAlign: 'right' as const, }, }; const statusConfig: Record = { ssr: { icon: 'SSR', color: colors.text.muted, bg: colors.background.light }, pending: { icon: 'WAIT', color: colors.semantic.warning, bg: `${colors.semantic.warning}20` }, hydrating: { icon: 'HYDR', color: colors.semantic.info, bg: `${colors.semantic.info}20` }, hydrated: { icon: 'READY', color: colors.semantic.success, bg: `${colors.semantic.success}20` }, error: { icon: 'ERR', color: colors.semantic.error, bg: `${colors.semantic.error}20` }, }; const strategyLabels: Record = { load: '즉시', idle: 'Idle', visible: 'Visible', media: 'Media', never: 'Never', }; // ============================================================================ // Props // ============================================================================ export interface IslandsPanelProps { islands: IslandSnapshot[]; } // ============================================================================ // Component // ============================================================================ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement { // Statistics const stats = useMemo(() => { const total = islands.length; const hydrated = islands.filter((i) => i.status === 'hydrated').length; const pending = islands.filter((i) => i.status === 'pending' || i.status === 'hydrating').length; const errors = islands.filter((i) => i.status === 'error').length; const totalHydrateTime = islands.reduce((sum, i) => { if (i.hydrateStartTime && i.hydrateEndTime) { return sum + (i.hydrateEndTime - i.hydrateStartTime); } return sum; }, 0); return { total, hydrated, pending, errors, totalHydrateTime }; }, [islands]); // Max hydration time for scaling const maxHydrateTime = useMemo(() => { return Math.max( 100, ...islands.map((i) => i.hydrateStartTime && i.hydrateEndTime ? i.hydrateEndTime - i.hydrateStartTime : 0 ) ); }, [islands]); if (islands.length === 0) { return (

아직 hydration 이벤트가 없습니다.
정적 상태는 mandu.runtime.status에서 확인하세요.

); } return (
{/* Header Stats */}
Mounts: {stats.total}
Ready {stats.hydrated}
{stats.pending > 0 && (
Pending {stats.pending}
)} {stats.errors > 0 && (
Error {stats.errors}
)}
총 시간: {stats.totalHydrateTime.toFixed(0)}ms
{/* Timeline */}
{islands.map((island) => { const status = statusConfig[island.status] ?? statusConfig.pending; const hydrateTime = island.hydrateStartTime && island.hydrateEndTime ? island.hydrateEndTime - island.hydrateStartTime : null; const timePercent = hydrateTime ? (hydrateTime / maxHydrateTime) * 100 : 0; return (
{status.icon}
{island.name}
{island.status} {strategyLabels[island.strategy] ?? island.strategy} {island.bundleSize && ( {(island.bundleSize / 1024).toFixed(1)}KB )}
{/* Time Bar */} {hydrateTime !== null && ( <>
100 ? colors.semantic.warning : colors.semantic.success, }} />
{hydrateTime.toFixed(0)}ms )} {hydrateTime === null && island.status === 'hydrating' && ( 진행중... )}
); })}
); }