diff --git a/frontend/src/components/Runtime/RuntimeControls.tsx b/frontend/src/components/Runtime/RuntimeControls.tsx index 1d3a8ee..82f9736 100644 --- a/frontend/src/components/Runtime/RuntimeControls.tsx +++ b/frontend/src/components/Runtime/RuntimeControls.tsx @@ -15,6 +15,7 @@ import { mdiCloudDownload, mdiCloudCheck, mdiCloudOff, + mdiLoading, mdiFullscreen, mdiFullscreenExit, mdiDelete, @@ -61,6 +62,27 @@ const buttonColors = { }, }; +/** + * Keyframes for spin animation (injected once) + */ +const spinKeyframes = ` +@keyframes runtime-controls-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +`; + +// Inject keyframes into document head (only once) +if (typeof document !== 'undefined') { + const styleId = 'runtime-controls-spin-style'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = spinKeyframes; + document.head.appendChild(style); + } +} + /** * Icon component using fixed pixel sizes */ @@ -68,10 +90,12 @@ function ControlIcon({ path, size = 20, fill = 'currentColor', + spinning = false, }: { path: string; size?: number; fill?: string; + spinning?: boolean; }) { return ( @@ -97,12 +122,14 @@ function ControlButton({ onClick, disabled = false, title, + spinning = false, }: { icon: string; color?: 'info' | 'success' | 'danger' | 'warning'; onClick: () => void; disabled?: boolean; title?: string; + spinning?: boolean; }) { const colors = buttonColors[color]; @@ -140,7 +167,7 @@ function ControlButton({ e.currentTarget.style.borderColor = colors.border; }} > - + ); } @@ -267,19 +294,21 @@ function OfflineControl({ } }; - // Determine icon and color + // Determine icon, color, and spinning state let icon = mdiCloudDownload; let color: 'info' | 'success' | 'danger' | 'warning' = 'info'; let title = 'Download for offline'; + let spinning = false; if (isDownloaded) { icon = mdiCloudCheck; color = 'success'; title = 'Available offline'; } else if (isDownloading) { - icon = mdiCloudDownload; + icon = mdiLoading; color = 'info'; title = `Downloading ${progress}%`; + spinning = true; } else if (status === 'error') { icon = mdiCloudOff; color = 'danger'; @@ -304,6 +333,7 @@ function OfflineControl({ onClick={handleClick} disabled={!canStore(estimatedSize) && !isDownloaded} title={title} + spinning={spinning} /> {isDownloaded && (