Skip to content

Commit ca43a6a

Browse files
chore: add loading overlay
1 parent 4a40403 commit ca43a6a

File tree

6 files changed

+197
-132
lines changed

6 files changed

+197
-132
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@docsearch/react": "^3.8.3",
3131
"@headlessui/react": "^1.7.0",
3232
"@radix-ui/react-context-menu": "^2.1.5",
33-
"@webcontainer/react": "^0.0.7",
33+
"@webcontainer/react": "^0.0.8",
3434
"body-scroll-lock": "^3.1.3",
3535
"classnames": "^2.2.6",
3636
"debounce": "^1.2.1",

src/components/MDX/Sandpack/LoadingOverlay.tsx

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,138 @@ import {useEffect} from 'react';
1212

1313
const 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+
15134
export 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

src/components/MDX/Sandpack/SandpackRSCRoot.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ function SandpackRSCRoot(props: SandpackProps) {
9797
theme={CustomTheme}
9898
options={{
9999
autorun,
100-
initMode: 'user-visible',
101-
initModeObserverOptions: {rootMargin: '1400px 0px'},
100+
initMode: 'immediate',
102101
}}>
103102
<CustomPreset providedFiles={Object.keys(files)} />
104103
</SandpackProvider>

src/components/MDX/Sandpack/SandpackRoot.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ function SandpackRoot(props: SandpackProps) {
9797
theme={CustomTheme}
9898
options={{
9999
autorun,
100-
initMode: 'user-visible',
101-
initModeObserverOptions: {rootMargin: '1400px 0px'},
100+
initMode: 'immediate',
102101
}}>
103102
<CustomPreset providedFiles={Object.keys(files)} />
104103
</SandpackProvider>

src/styles/sandpack.css

Lines changed: 61 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -305,106 +305,6 @@ html.dark .sp-wrapper {
305305
/**
306306
* Loading & error overlay component
307307
*/
308-
.sandpack .sp-cube-wrapper {
309-
background-color: var(--sp-colors-surface1);
310-
position: absolute;
311-
right: var(--sp-space-2);
312-
bottom: var(--sp-space-2);
313-
z-index: var(--sp-zIndices-top);
314-
width: 32px;
315-
height: 32px;
316-
border-radius: var(--sp-border-radius);
317-
}
318-
319-
.sandpack .sp-button {
320-
display: flex;
321-
align-items: center;
322-
margin: auto;
323-
width: 100%;
324-
height: 100%;
325-
}
326-
327-
.sandpack .sp-button svg {
328-
min-width: var(--sp-space-5);
329-
width: var(--sp-space-5);
330-
height: var(--sp-space-5);
331-
margin: auto;
332-
}
333-
334-
.sandpack .sp-cube-wrapper .sp-cube {
335-
display: flex;
336-
}
337-
338-
.sandpack .sp-cube-wrapper .sp-button {
339-
display: none;
340-
}
341-
342-
.sandpack .sp-cube-wrapper:hover .sp-button {
343-
display: block;
344-
}
345-
346-
.sandpack .sp-cube-wrapper:hover .sp-cube {
347-
display: none;
348-
}
349-
350-
.sandpack .sp-cube {
351-
transform: translate(-4px, 9px) scale(0.13, 0.13);
352-
}
353-
354-
.sandpack .sp-cube * {
355-
position: absolute;
356-
width: 96px;
357-
height: 96px;
358-
}
359-
360-
@keyframes cubeRotate {
361-
0% {
362-
transform: rotateX(-25.5deg) rotateY(45deg);
363-
}
364-
365-
100% {
366-
transform: rotateX(-25.5deg) rotateY(405deg);
367-
}
368-
}
369-
370-
.sandpack .sp-sides {
371-
animation: cubeRotate 1s linear infinite;
372-
animation-fill-mode: forwards;
373-
transform-style: preserve-3d;
374-
transform: rotateX(-25.5deg) rotateY(45deg);
375-
}
376-
377-
.sandpack .sp-sides * {
378-
border: 10px solid var(--sp-colors-clickable);
379-
border-radius: 8px;
380-
background: var(--sp-colors-surface1);
381-
}
382-
383-
.sandpack .sp-sides .top {
384-
transform: rotateX(90deg) translateZ(44px);
385-
transform-origin: 50% 50%;
386-
}
387-
.sandpack .sp-sides .bottom {
388-
transform: rotateX(-90deg) translateZ(44px);
389-
transform-origin: 50% 50%;
390-
}
391-
.sandpack .sp-sides .front {
392-
transform: rotateY(0deg) translateZ(44px);
393-
transform-origin: 50% 50%;
394-
}
395-
.sandpack .sp-sides .back {
396-
transform: rotateY(-180deg) translateZ(44px);
397-
transform-origin: 50% 50%;
398-
}
399-
.sandpack .sp-sides .left {
400-
transform: rotateY(-90deg) translateZ(44px);
401-
transform-origin: 50% 50%;
402-
}
403-
.sandpack .sp-sides .right {
404-
transform: rotateY(90deg) translateZ(44px);
405-
transform-origin: 50% 50%;
406-
}
407-
408308
.sandpack .sp-overlay {
409309
@apply bg-card;
410310
position: absolute;
@@ -612,3 +512,64 @@ html.dark .sp-devtools > div {
612512
.sp-loading .sp-icon-standalone span {
613513
display: none;
614514
}
515+
516+
/**
517+
* Boot progress checklist
518+
*/
519+
.sp-boot-checklist {
520+
display: flex;
521+
flex-direction: column;
522+
align-items: flex-start;
523+
justify-content: center;
524+
height: 100%;
525+
margin: 0 auto;
526+
width: fit-content;
527+
gap: 10px;
528+
}
529+
530+
.sp-boot-step {
531+
display: flex;
532+
align-items: center;
533+
gap: 8px;
534+
font-family: var(--sp-font-body);
535+
font-size: 13px;
536+
line-height: 1;
537+
transition: color 300ms ease, opacity 300ms ease;
538+
}
539+
540+
.sp-boot-step-pending {
541+
color: var(--sp-colors-clickable);
542+
opacity: 0.4;
543+
}
544+
545+
.sp-boot-step-active {
546+
color: var(--sp-colors-accent);
547+
opacity: 1;
548+
}
549+
550+
.sp-boot-step-done {
551+
color: var(--sp-colors-clickable);
552+
opacity: 0.65;
553+
}
554+
555+
.sp-boot-step-icon {
556+
display: flex;
557+
align-items: center;
558+
justify-content: center;
559+
width: 16px;
560+
height: 16px;
561+
flex-shrink: 0;
562+
}
563+
564+
@keyframes bootSpinnerRotate {
565+
from {
566+
transform: rotate(0deg);
567+
}
568+
to {
569+
transform: rotate(360deg);
570+
}
571+
}
572+
573+
.sp-boot-spinner {
574+
animation: bootSpinnerRotate 0.8s linear infinite;
575+
}

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,10 +1995,10 @@
19951995
resolved "https://registry.yarnpkg.com/@webcontainer/api/-/api-1.6.1.tgz#8f4034b858279595b3b6aa2652b38af713924dfc"
19961996
integrity sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==
19971997

1998-
"@webcontainer/react@^0.0.7":
1999-
version "0.0.7"
2000-
resolved "https://registry.yarnpkg.com/@webcontainer/react/-/react-0.0.7.tgz#faea3e7deb973b1e1c6ff0147962b3308fc19cd1"
2001-
integrity sha512-uS29mkMTofrbIZFRnlvM2OW/P5RSRUI2xo3+wiECCFMX4UrefeQUTeEBcGL4xVvhxRnrmmU0TXtFQ1lbeIqekw==
1998+
"@webcontainer/react@^0.0.8":
1999+
version "0.0.8"
2000+
resolved "https://registry.yarnpkg.com/@webcontainer/react/-/react-0.0.8.tgz#45fcc8e80931e5cca4862cbbae9aef776e07d250"
2001+
integrity sha512-23ZSkK9ikgi6SveDa/TEbnRsTljC/Gqh75oCmWGUr0HSVFneie3mpqb3XQNsQFpJWzM+jH2s8FaJapoGN1QySQ==
20022002
dependencies:
20032003
"@codemirror/commands" "^6.10.3"
20042004
"@codemirror/lang-css" "^6.3.1"

0 commit comments

Comments
 (0)