@@ -12,22 +12,138 @@ import {useEffect} from 'react';
1212
1313const FADE_ANIMATION_DURATION = 200 ;
1414
15+ type BootPhase = 'booting' | 'installing' | 'starting' ;
16+
17+ const BOOT_PHASES : BootPhase [ ] = [ 'booting' , 'installing' , 'starting' ] ;
18+
19+ function isBootPhase ( status : string ) : status is BootPhase {
20+ return BOOT_PHASES . includes ( status as BootPhase ) ;
21+ }
22+
23+ const BOOT_STEPS : { phase : BootPhase ; label : string } [ ] = [
24+ { phase : 'booting' , label : 'Booting sandbox' } ,
25+ { phase : 'installing' , label : 'Installing dependencies' } ,
26+ { phase : 'starting' , label : 'Starting dev server' } ,
27+ ] ;
28+
29+ type StepState = 'pending' | 'active' | 'done' ;
30+
31+ function getStepState (
32+ stepPhase : BootPhase ,
33+ currentPhase : BootPhase
34+ ) : StepState {
35+ const stepIndex = BOOT_PHASES . indexOf ( stepPhase ) ;
36+ const currentIndex = BOOT_PHASES . indexOf ( currentPhase ) ;
37+ if ( currentIndex > stepIndex ) return 'done' ;
38+ if ( currentIndex === stepIndex ) return 'active' ;
39+ return 'pending' ;
40+ }
41+
42+ function CheckIcon ( ) {
43+ return (
44+ < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" >
45+ < circle cx = "8" cy = "8" r = "7" stroke = "currentColor" strokeWidth = "1.5" />
46+ < path
47+ d = "M5 8.5l2 2 4-4.5"
48+ stroke = "currentColor"
49+ strokeWidth = "1.5"
50+ strokeLinecap = "round"
51+ strokeLinejoin = "round"
52+ />
53+ </ svg >
54+ ) ;
55+ }
56+
57+ function SpinnerIcon ( ) {
58+ return (
59+ < svg
60+ className = "sp-boot-spinner"
61+ width = "16"
62+ height = "16"
63+ viewBox = "0 0 16 16"
64+ fill = "none" >
65+ < circle
66+ cx = "8"
67+ cy = "8"
68+ r = "6.5"
69+ stroke = "currentColor"
70+ strokeWidth = "1.5"
71+ opacity = "0.2"
72+ />
73+ < path
74+ d = "M8 1.5A6.5 6.5 0 0 1 14.5 8"
75+ stroke = "currentColor"
76+ strokeWidth = "1.5"
77+ strokeLinecap = "round"
78+ />
79+ </ svg >
80+ ) ;
81+ }
82+
83+ function CircleIcon ( ) {
84+ return (
85+ < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" >
86+ < circle
87+ cx = "8"
88+ cy = "8"
89+ r = "6.5"
90+ stroke = "currentColor"
91+ strokeWidth = "1.5"
92+ opacity = "0.25"
93+ />
94+ </ svg >
95+ ) ;
96+ }
97+
98+ function BootProgressChecklist ( {
99+ phase,
100+ opacity = 1 ,
101+ } : {
102+ phase : BootPhase | 'complete' ;
103+ opacity ?: number ;
104+ } ) {
105+ return (
106+ < div
107+ className = "sp-overlay sp-loading"
108+ style = { {
109+ opacity,
110+ transition : `opacity ${ FADE_ANIMATION_DURATION } ms ease-out` ,
111+ } } >
112+ < div className = "sp-boot-checklist" >
113+ { BOOT_STEPS . map ( ( { phase : stepPhase , label} ) => {
114+ const state =
115+ phase === 'complete' ? 'done' : getStepState ( stepPhase , phase ) ;
116+ return (
117+ < div
118+ key = { stepPhase }
119+ className = { `sp-boot-step sp-boot-step-${ state } ` } >
120+ < span className = "sp-boot-step-icon" >
121+ { state === 'done' && < CheckIcon /> }
122+ { state === 'active' && < SpinnerIcon /> }
123+ { state === 'pending' && < CircleIcon /> }
124+ </ span >
125+ < span className = "sp-boot-step-label" > { label } </ span >
126+ </ div >
127+ ) ;
128+ } ) }
129+ </ div >
130+ </ div >
131+ ) ;
132+ }
133+
15134export const LoadingOverlay = ( {
16135 dependenciesLoading,
17136 forceLoading,
18137} : {
19138 dependenciesLoading : boolean ;
20139 forceLoading : boolean ;
21140} & React . HTMLAttributes < HTMLDivElement > ) : React . ReactNode | null => {
141+ const { sandpack} = useSandpack ( ) ;
22142 const loadingOverlayState = useLoadingOverlayState (
23143 dependenciesLoading ,
24144 forceLoading
25145 ) ;
26146
27- if ( loadingOverlayState === 'HIDDEN' ) {
28- return null ;
29- }
30-
31147 if ( loadingOverlayState === 'TIMEOUT' ) {
32148 return (
33149 < div className = "sp-overlay sp-error" >
@@ -46,29 +162,19 @@ export const LoadingOverlay = ({
46162 ) ;
47163 }
48164
165+ if ( isBootPhase ( sandpack . status ) ) {
166+ return < BootProgressChecklist phase = { sandpack . status } /> ;
167+ }
168+
169+ if ( loadingOverlayState === 'HIDDEN' ) {
170+ return null ;
171+ }
172+
49173 const stillLoading =
50174 loadingOverlayState === 'LOADING' || loadingOverlayState === 'PRE_FADING' ;
51175
52176 return (
53- < div
54- className = "sp-overlay sp-loading"
55- style = { {
56- opacity : stillLoading ? 1 : 0 ,
57- transition : `opacity ${ FADE_ANIMATION_DURATION } ms ease-out` ,
58- } } >
59- < div className = "sp-cube-wrapper" title = "Open in StackBlitz" >
60- < div className = "sp-cube" >
61- < div className = "sp-sides" >
62- < div className = "top" />
63- < div className = "right" />
64- < div className = "bottom" />
65- < div className = "left" />
66- < div className = "front" />
67- < div className = "back" />
68- </ div >
69- </ div >
70- </ div >
71- </ div >
177+ < BootProgressChecklist phase = "complete" opacity = { stillLoading ? 1 : 0 } />
72178 ) ;
73179} ;
74180
0 commit comments