+
);
};
-const useButtonAnimation = (state: StateType) => {
+const BASE_ICON_STYLES: MotionStyle = {
+ display: 'none',
+ rotate: 45,
+};
+const BASE_ICON_OPTIONS: DynamicAnimationOptions = {
+ duration: 0.1,
+};
+const CURRENT_ICON_STYLES: MotionStyle = {
+ display: 'block',
+ position: 'absolute',
+ rotate: 0,
+};
+const CURRENT_ICON_OPTIONS: DynamicAnimationOptions = {
+ duration: 0.1,
+ delay: 0.1,
+};
+const BUTTON_PRIMARY_STYLES: MotionStyle = {
+ backgroundColor: 'hsl(var(--primary))',
+ color: 'hsl(var(--primary-foreground))',
+ boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
+};
+
+const BUTTON_DESTRUCTIVE_STYLES: MotionStyle = {
+ backgroundColor: 'hsl(var(--destructive))',
+ color: 'hsl(var(--destructive-foreground))',
+ boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
+};
+const BUTTON_OPTIONS = {
+ duration: 0.3,
+ at: '>',
+};
+
+const useButtonAnimation = (state: ExecutionStatusType) => {
const [scope, animate] = useAnimate();
useEffect(() => {
switch (state) {
case 'idle':
animate([
- ['#icon-pause', { display: 'none', rotate: 90 }, { duration: 0.1 }],
- ['#icon-play', { display: 'inline', rotate: 0 }, { duration: 0.2 }],
- [
- 'button',
- {
- backgroundColor: 'hsl(var(--primary))',
- color: 'hsl(var(--primary-foreground))',
- boxShadow:
- '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
- },
- { duration: 0.3, at: '>' },
- ],
+ ['#icon-pause', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-running', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-loading', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-play', CURRENT_ICON_STYLES, CURRENT_ICON_OPTIONS],
+ ['button', BUTTON_PRIMARY_STYLES, BUTTON_OPTIONS],
+ ]);
+ break;
+ case 'waiting':
+ animate([
+ ['#icon-pause', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-running', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-play', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-loading', CURRENT_ICON_STYLES, CURRENT_ICON_OPTIONS],
+ ['button', BUTTON_PRIMARY_STYLES, BUTTON_OPTIONS],
]);
break;
case 'running':
animate([
- ['#icon-pause', { display: 'none', rotate: 90 }, { duration: 0.1 }],
- ['#icon-play', { display: 'inline', rotate: 0 }, { duration: 0.2 }],
- [
- 'button',
- {
- backgroundColor: 'hsl(var(--primary))',
- color: 'hsl(var(--primary-foreground))',
- boxShadow:
- '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
- },
- { duration: 0.3, at: '>' },
- ],
+ ['#icon-play', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-pause', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-loading', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-running', CURRENT_ICON_STYLES, CURRENT_ICON_OPTIONS],
+ ['button', BUTTON_PRIMARY_STYLES, BUTTON_OPTIONS],
]);
break;
case 'overflow':
animate([
- ['#icon-play', { display: 'none', rotate: 90 }, { duration: 0.1 }],
- ['#icon-pause', { display: 'inline', rotate: 0 }, { duration: 0.2 }],
- [
- 'button',
- {
- backgroundColor: 'hsl(var(--destructive))',
- color: 'hsl(var(--destructive-foreground))',
- boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
- },
- { duration: 0.3, at: '>' },
- ],
+ ['#icon-play', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-running', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-loading', BASE_ICON_STYLES, BASE_ICON_OPTIONS],
+ ['#icon-pause', CURRENT_ICON_STYLES, CURRENT_ICON_OPTIONS],
+ ['button', BUTTON_DESTRUCTIVE_STYLES, BUTTON_OPTIONS],
]);
break;
}
@@ -190,3 +152,51 @@ const useButtonAnimation = (state: StateType) => {
return scope;
};
+
+interface LoadingIconProps {
+ id?: string;
+}
+
+const LoadingIcon: React.FC
= ({ id }) => {
+ return (
+
+ );
+};
diff --git a/src/features/editor/hooks/use-execution-layer.tsx b/src/features/editor/hooks/use-execution-layer.tsx
new file mode 100644
index 0000000..1650dfe
--- /dev/null
+++ b/src/features/editor/hooks/use-execution-layer.tsx
@@ -0,0 +1,14 @@
+import { useContext } from 'react';
+
+import { ExecutionLayerContext } from '../providers/execution-layer';
+
+export const useExecutionLayer = () => {
+ const context = useContext(ExecutionLayerContext);
+
+ if (context === undefined)
+ throw new Error(
+ 'useExecutionLayer must be used within a ExecutionLayerProvider',
+ );
+
+ return context;
+};
diff --git a/src/features/editor/providers/execution-layer.tsx b/src/features/editor/providers/execution-layer.tsx
new file mode 100644
index 0000000..7499d90
--- /dev/null
+++ b/src/features/editor/providers/execution-layer.tsx
@@ -0,0 +1,130 @@
+import React, { createContext, useEffect, useState } from 'react';
+import { toast } from 'sonner';
+
+import useDebounce from '@/hooks/use-debounce';
+import useTimeoutFn from '@/hooks/use-timeout-fn';
+
+import { useUntrustedMode } from '../hooks/use-untrusted-mode';
+import { useCodeStore, useLogsStore } from '../stores/editor';
+import { useSettingsStore } from '../stores/settings';
+import { runJs, stopJs } from '../utils/engine/controller';
+
+const DEFAULT_OVERFLOW_TIMEOUT = 4000;
+
+export type ExecutionStatusType = 'idle' | 'waiting' | 'running' | 'overflow';
+
+type ExecutionLayerState = {
+ status: ExecutionStatusType;
+ runExec: () => void;
+ stopExec: () => void;
+};
+
+const initialState: ExecutionLayerState = {
+ status: 'idle',
+ runExec: () => null,
+ stopExec: () => null,
+};
+
+export const ExecutionLayerContext =
+ createContext(initialState);
+
+interface Props {
+ children: React.ReactNode;
+}
+
+// eslint-disable-next-line max-lines-per-function
+export const ExecutionLayerProvider: React.FC = ({ children }) => {
+ const { persist_logs, auto_run, auto_run_timeout } = useSettingsStore();
+ const { code } = useCodeStore();
+ const { clearLogs } = useLogsStore();
+ const { isUntrustedMode, isUnedited } = useUntrustedMode();
+
+ const [status, setStatus] = useState(
+ initialState.status,
+ );
+
+ const [_1, cancelOveflowTimeout, resetOverflowTimeout] = useTimeoutFn(() => {
+ if (!(status === 'running')) return;
+ setStatus('overflow');
+ }, DEFAULT_OVERFLOW_TIMEOUT);
+
+ const [isReadyAutoRunDebounce, cancelAutoRunDebounce] = useDebounce(
+ () => {
+ if (
+ status !== 'running' &&
+ status !== 'overflow' &&
+ auto_run &&
+ !isUnedited &&
+ !isUntrustedMode
+ ) {
+ runExec();
+ }
+ },
+ auto_run_timeout,
+ [code],
+ );
+
+ useEffect(() => {
+ const isReady = isReadyAutoRunDebounce();
+ if (
+ typeof isReady === 'boolean' &&
+ !isReady &&
+ auto_run &&
+ !isUnedited &&
+ !isUntrustedMode
+ ) {
+ setStatus('waiting');
+ }
+ }, [isReadyAutoRunDebounce, code]);
+
+ useEffect(() => {
+ if (!auto_run) {
+ cancelAutoRunDebounce();
+ }
+ }, [auto_run]);
+
+ const runExec = async () => {
+ try {
+ if (code.length === 0) {
+ throw '01';
+ }
+ if (status === 'running' || status === 'overflow') {
+ stopJs();
+ }
+ resetOverflowTimeout();
+ if (!persist_logs) clearLogs();
+ setStatus('running');
+ await runJs(code);
+ } catch (e) {
+ console.error(e);
+ if (e === '01') {
+ toast.warning('Nothing to execute!');
+ }
+ } finally {
+ resetExecutionLayer();
+ }
+ };
+
+ const stopExec = () => {
+ stopJs();
+ resetExecutionLayer();
+ };
+
+ const resetExecutionLayer = () => {
+ setStatus('idle');
+ cancelOveflowTimeout();
+ cancelAutoRunDebounce();
+ };
+
+ const value = {
+ status,
+ runExec,
+ stopExec,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/features/editor/providers/index.tsx b/src/features/editor/providers/index.tsx
index 5a1cfea..f00d7cc 100644
--- a/src/features/editor/providers/index.tsx
+++ b/src/features/editor/providers/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { EditorBehaviorProvider } from './editor-behavior';
+import { ExecutionLayerProvider } from './execution-layer';
import { HelpProvider } from './help-provider';
import { MetatagsProvider } from './meta-provider';
import { SettingsDialogProvider } from './settings-dialog-provider';
@@ -17,7 +18,9 @@ const EditorProviders: React.FC = ({ children }) => {
- <>{children}>
+
+ <>{children}>
+