Skip to content

Commit

Permalink
fix: animate action button
Browse files Browse the repository at this point in the history
  • Loading branch information
Pkcarreno committed Jan 6, 2025
1 parent dc9ecba commit c07a1a0
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 31 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"eslint-linter-browserify": "^9.17.0",
"globals": "^15.13.0",
"js-base64": "^3.7.7",
"motion": "^11.16.0",
"only-allow": "^1.2.1",
"pretty-ms": "^9.2.0",
"quickjs-emscripten-core": "0.31.0",
Expand Down
60 changes: 60 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 126 additions & 31 deletions src/features/editor/components/header/action-buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { PauseIcon, PlayIcon } from '@radix-ui/react-icons';
import { useEffect, useState } from 'react';
import { useAnimate } from 'motion/react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';

import { Button } from '@/components/ui/button';
import { useUntrustedMode } from '@/features/editor/hooks/use-untrusted-mode';
import { useCodeStore, useLogsStore } from '@/features/editor/stores/editor';
import { useSettingsStore } from '@/features/editor/stores/settings';
import useDebounce from '@/hooks/use-debounce';
import useTimeoutFn from '@/hooks/use-timeout-fn';
import { cn } from '@/lib/utils';

import { runJs, stopJs } from '../../utils/engine/controller';

// eslint-disable-next-line max-lines-per-function
export const ActionButtons = () => {
const { persist_logs, auto_run, auto_run_timeout } = useSettingsStore();
const { code } = useCodeStore();
Expand Down Expand Up @@ -63,35 +63,130 @@ export const ActionButtons = () => {
clearStopButtonState();
};

if (auto_run) {
return (
<Button
variant="destructive"
disabled={!showStopButton}
onClick={stopExecution}
size="icon"
>
<PauseIcon className="size-5" />
</Button>
);
}

if (isExecuting && showStopButton)
return (
<Button variant="destructive" className="gap-1" onClick={stopExecution}>
<span className="font-semibold">Stop</span>
<PauseIcon className="size-5" />
</Button>
);
const currentState = useMemo(() => {
if (isExecuting && showStopButton) return 'overflow';
if (isExecuting && !showStopButton) return 'running';

return 'idle';
}, [isExecuting, showStopButton]);

return (
<Button
className="gap-1"
disabled={auto_run || isExecuting || isUntrustedMode}
onClick={handleRunCode}
>
<span className="font-semibold">Play</span>
<PlayIcon className="size-5" />
</Button>
<AnimatedActionButton
state={currentState}
stopExec={stopExecution}
runExec={handleRunCode}
/>
);
};

type StateType = 'idle' | 'waiting' | 'running' | 'overflow';

export type AnimatedActionButtonProps =
React.ButtonHTMLAttributes<HTMLButtonElement> & {
state: StateType;
stopExec: () => void;
runExec: () => void;
};

export const AnimatedActionButton: React.FC<AnimatedActionButtonProps> = ({
state,
runExec,
stopExec,
}) => {
useEffect(() => {
console.log('state', state);
}, [state]);

const scope = useButtonAnimation(state);

const isDisabled = useMemo(() => {
if (state === 'running') return true;
return false;
}, [state]);

const handleExecution = useCallback(() => {
if (state === 'idle') return runExec();
return stopExec();
}, [state, stopExec, runExec]);

const classNamesByState = useMemo(() => {
if (state === 'overflow')
return 'bg-destructive text-destructive-foreground shadow-sm hover:!bg-destructive/90';

return 'bg-primary text-primary-foreground shadow hover:!bg-primary/90';
}, [state]);

return (
<div ref={scope}>
<button
className={cn(
'focus-visible:ring-ring [&_svg]:shrink-0" inline-flex size-9 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4',
classNamesByState,
)}
onClick={handleExecution}
disabled={isDisabled}
>
<PauseIcon id="icon-pause" className="size-5" />
<PlayIcon id="icon-play" className="size-5" />
</button>
</div>
);
};

const useButtonAnimation = (state: StateType) => {
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: '>' },
],
]);
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: '>' },
],
]);
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: '>' },
],
]);
break;
}
}, [state]);

return scope;
};

0 comments on commit c07a1a0

Please sign in to comment.