Skip to content

Commit 87a64f5

Browse files
jammy0903claude
andcommitted
feat(playground): add stdin support, localStorage persist, timeout UI, and redesigned controls
- stdin: collapsible input textarea for C language, passed to /trace endpoint - persist: zustand persist middleware saves codes/stdins/language to localStorage - timeout UI: AbortController cancel button with elapsed time counter, 30s warning - all 5 simulators accept signal for request cancellation - redesigned header: pill-shaped language tabs with space-evenly layout, pill-shaped Run/Reset buttons, mobile 2-row layout with space-between controls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad638ea commit 87a64f5

11 files changed

Lines changed: 462 additions & 255 deletions

File tree

packages/frontend/src/features/playground/PlaygroundPage.tsx

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
* Right 50%: Memory Visualization
55
*/
66

7-
import { useEffect, useState, useMemo } from 'react';
7+
import { useEffect, useState, useMemo, useCallback } from 'react';
88
import { useTranslation } from 'react-i18next';
99
import { useIsMobile } from '@/hooks';
10-
import { Play, Layers } from 'lucide-react';
10+
import { Play, Layers, ChevronDown, ChevronRight as ChevronRightIcon, Terminal } from 'lucide-react';
1111
import { LanguageTabs } from './components/LanguageTabs';
1212
import { CodeMirrorEditor } from '@/features/visualizers/shared/components/CodeMirrorEditor';
1313
import { StepControls } from './components/StepControls';
@@ -29,7 +29,7 @@ const MAX_EDITOR_HEIGHT = 500;
2929

3030
export function PlaygroundPage() {
3131
const { t } = useTranslation();
32-
const { steps, currentStepIndex, error, language, setCode } = usePlaygroundStore();
32+
const { steps, currentStepIndex, error, language, setCode, stdins, setStdin } = usePlaygroundStore();
3333
const code = useCurrentCode();
3434
const setPageTitle = useStore((s) => s.setPageTitle);
3535
const currentTheme = useThemeStore((s) => s.theme);
@@ -40,6 +40,13 @@ export function PlaygroundPage() {
4040
const currentStep = steps[currentStepIndex];
4141
const hasSteps = steps.length > 0;
4242

43+
// stdin 접이식 상태
44+
const [stdinOpen, setStdinOpen] = useState(false);
45+
const currentStdin = stdins[language] || '';
46+
const handleStdinChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
47+
setStdin(e.target.value);
48+
}, [setStdin]);
49+
4350
// Flow/Memory 탭 상태
4451
const [activeTab, setActiveTab] = useState<'flow' | 'memory'>('flow');
4552

@@ -88,21 +95,16 @@ export function PlaygroundPage() {
8895
<div style={{ backgroundColor: colors.pageBg, minHeight: '100vh', display: 'flex', flexDirection: 'column', padding: '16px' }}>
8996
{/* Code Section */}
9097
<div style={{ backgroundColor: colors.panelBg, flexShrink: 0 }}>
91-
{/* Header */}
92-
<div
93-
style={{
94-
height: '40px',
95-
padding: '0 8px',
96-
display: 'flex',
97-
alignItems: 'center',
98-
justifyContent: 'space-between',
99-
borderBottom: `1px solid ${colors.border}`,
100-
backgroundColor: colors.headerBg,
101-
gap: '4px',
102-
}}
103-
>
98+
{/* Header: 모바일 2줄 — 탭 꽉 채움 + 컨트롤 */}
99+
<div style={{
100+
backgroundColor: colors.headerBg,
101+
borderBottom: `1px solid ${colors.border}`,
102+
padding: '8px 10px',
103+
display: 'flex',
104+
flexDirection: 'column',
105+
gap: '8px',
106+
}}>
104107
<LanguageTabs isMobile={true} />
105-
{/* Run + Reset + Navigation 버튼 (하단에도 추가로 표시) */}
106108
<StepControls isMobile={true} showRun={true} showReset={true} showNavigation={true} />
107109
</div>
108110

@@ -125,6 +127,55 @@ export function PlaygroundPage() {
125127
/>
126128
)}
127129
</div>
130+
131+
{/* stdin 입력 (접이식) */}
132+
{language === 'c' && (
133+
<div style={{ borderTop: `1px solid ${colors.border}` }}>
134+
<button
135+
onClick={() => setStdinOpen(!stdinOpen)}
136+
style={{
137+
width: '100%',
138+
display: 'flex',
139+
alignItems: 'center',
140+
gap: '4px',
141+
padding: '6px 8px',
142+
fontSize: '10px',
143+
fontWeight: 600,
144+
color: currentStdin ? colors.accent : colors.textMuted,
145+
background: 'transparent',
146+
border: 'none',
147+
cursor: 'pointer',
148+
}}
149+
>
150+
{stdinOpen ? <ChevronDown size={10} /> : <ChevronRightIcon size={10} />}
151+
<Terminal size={10} />
152+
Input (stdin)
153+
{currentStdin && <span style={{ fontSize: '9px', opacity: 0.7 }}>*</span>}
154+
</button>
155+
{stdinOpen && (
156+
<textarea
157+
value={currentStdin}
158+
onChange={handleStdinChange}
159+
placeholder="Enter input values, one per line..."
160+
rows={3}
161+
style={{
162+
width: '100%',
163+
padding: '6px 8px',
164+
fontSize: '11px',
165+
fontFamily: 'monospace',
166+
backgroundColor: colors.panelBg,
167+
color: colors.textMuted,
168+
border: 'none',
169+
borderTop: `1px solid ${colors.border}`,
170+
outline: 'none',
171+
resize: 'vertical',
172+
minHeight: '48px',
173+
maxHeight: '120px',
174+
}}
175+
/>
176+
)}
177+
</div>
178+
)}
128179
</div>
129180

130181
{/* Visualization Section - Flow Only */}
@@ -314,22 +365,19 @@ export function PlaygroundPage() {
314365
minHeight: '133px',
315366
}}
316367
>
317-
{/* Code Header */}
318-
<div
319-
style={{
320-
height: '48px',
321-
padding: '0 16px',
322-
display: 'flex',
323-
alignItems: 'center',
324-
justifyContent: 'space-between',
325-
borderBottom: `1px solid ${colors.border}`,
326-
backgroundColor: colors.headerBg,
327-
flexShrink: 0,
328-
gap: '8px',
329-
}}
330-
>
368+
{/* Code Header: flex-wrap — 넓으면 1줄, 좁으면 자연스럽게 줄바꿈 */}
369+
<div style={{
370+
backgroundColor: colors.headerBg,
371+
borderBottom: `1px solid ${colors.border}`,
372+
flexShrink: 0,
373+
padding: '10px 16px',
374+
display: 'flex',
375+
flexWrap: 'wrap',
376+
alignItems: 'center',
377+
gap: '10px',
378+
}}>
331379
<LanguageTabs />
332-
{/* Run + Reset + Navigation 버튼 (헤더) */}
380+
<div style={{ width: '1px', height: '22px', background: colors.border, flexShrink: 0 }} />
333381
<StepControls showRun={true} showReset={true} showNavigation={true} />
334382
</div>
335383

@@ -353,6 +401,55 @@ export function PlaygroundPage() {
353401
)}
354402
</div>
355403

404+
{/* stdin 입력 (접이식) */}
405+
{language === 'c' && (
406+
<div style={{ borderTop: `1px solid ${colors.border}`, flexShrink: 0 }}>
407+
<button
408+
onClick={() => setStdinOpen(!stdinOpen)}
409+
style={{
410+
width: '100%',
411+
display: 'flex',
412+
alignItems: 'center',
413+
gap: '6px',
414+
padding: '8px 16px',
415+
fontSize: '12px',
416+
fontWeight: 600,
417+
color: currentStdin ? colors.accent : colors.textMuted,
418+
background: 'transparent',
419+
border: 'none',
420+
cursor: 'pointer',
421+
transition: 'color 0.15s',
422+
}}
423+
>
424+
{stdinOpen ? <ChevronDown size={12} /> : <ChevronRightIcon size={12} />}
425+
<Terminal size={12} />
426+
Input (stdin)
427+
{currentStdin && <span style={{ fontSize: '10px', opacity: 0.7 }}>*</span>}
428+
</button>
429+
{stdinOpen && (
430+
<textarea
431+
value={currentStdin}
432+
onChange={handleStdinChange}
433+
placeholder="Enter input values, one per line..."
434+
rows={3}
435+
style={{
436+
width: '100%',
437+
padding: '8px 16px',
438+
fontSize: '12px',
439+
fontFamily: 'monospace',
440+
backgroundColor: colors.panelBg,
441+
color: colors.textMuted,
442+
border: 'none',
443+
borderTop: `1px solid ${colors.border}`,
444+
outline: 'none',
445+
resize: 'vertical',
446+
minHeight: '60px',
447+
maxHeight: '150px',
448+
}}
449+
/>
450+
)}
451+
</div>
452+
)}
356453
</div>
357454

358455
{/* ===== Right Panel: Flow + Memory Tabs - 컨텐츠에 따라 늘어남 ===== */}

packages/frontend/src/features/playground/components/LanguageTabs.tsx

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
22
* LanguageTabs - 언어 선택 탭
3-
* 컴팩트 라이트 테마 스타일
4-
* 반응형 지원 (모바일에서 더 컴팩트)
3+
* 각 탭이 pill 모양, 전체 너비를 균등하게 차지
54
*/
65

76
import { memo, useMemo } from 'react';
@@ -11,8 +10,8 @@ import type { SupportedLanguage } from '@/types';
1110
const LANGUAGES: { id: SupportedLanguage; label: string; shortLabel: string; color: string }[] = [
1211
{ id: 'c', label: 'C', shortLabel: 'C', color: '#3b82f6' },
1312
{ id: 'cpp', label: 'C++', shortLabel: 'C++', color: '#6366f1' },
14-
{ id: 'python', label: 'Py', shortLabel: 'Py', color: '#22c55e' },
15-
{ id: 'java', label: 'Java', shortLabel: 'Ja', color: '#EC4899' },
13+
{ id: 'python', label: 'Python', shortLabel: 'Py', color: '#22c55e' },
14+
{ id: 'java', label: 'Java', shortLabel: 'Java', color: '#EC4899' },
1615
{ id: 'javascript', label: 'JS', shortLabel: 'JS', color: '#f59e0b' },
1716
];
1817

@@ -23,37 +22,30 @@ interface LanguageTabsProps {
2322
export const LanguageTabs = memo(function LanguageTabs({ isMobile = false }: LanguageTabsProps) {
2423
const { language, setLanguage } = usePlaygroundStore();
2524

26-
// 컨테이너 스타일 메모이제이션
2725
const containerStyle = useMemo(() => ({
2826
display: 'flex',
2927
alignItems: 'center',
30-
gap: '2px',
31-
padding: '2px',
32-
backgroundColor: 'var(--theme-memory-reset-bg)',
33-
borderRadius: isMobile ? '4px' : '6px',
34-
border: '1px solid var(--theme-memory-reset-border)',
28+
justifyContent: 'space-evenly',
29+
gap: isMobile ? '6px' : '6px',
30+
width: '100%',
3531
} as const), [isMobile]);
3632

37-
// 버튼 기본 스타일 (변하지 않는 속성들)
3833
const baseButtonStyle = useMemo(() => ({
3934
display: 'flex',
4035
alignItems: 'center',
41-
gap: isMobile ? '2px' : '4px',
42-
padding: isMobile ? '3px 5px' : '4px 8px',
43-
borderRadius: isMobile ? '3px' : '4px',
44-
fontSize: isMobile ? '10px' : '11px',
36+
justifyContent: 'center',
37+
gap: isMobile ? '5px' : '5px',
38+
padding: isMobile ? '7px 14px' : '6px 14px',
39+
borderRadius: '999px',
40+
fontSize: isMobile ? '12px' : '12px',
4541
fontWeight: 600,
46-
border: 'none',
42+
border: '1px solid transparent',
4743
cursor: 'pointer',
4844
transition: 'all 0.15s ease',
45+
whiteSpace: 'nowrap',
4946
} as const), [isMobile]);
5047

51-
// 인디케이터 기본 스타일
52-
const indicatorBaseStyle = useMemo(() => ({
53-
width: isMobile ? '4px' : '6px',
54-
height: isMobile ? '4px' : '6px',
55-
borderRadius: '50%',
56-
} as const), [isMobile]);
48+
const indicatorSize = isMobile ? '5px' : '6px';
5749

5850
return (
5951
<div style={containerStyle}>
@@ -65,14 +57,18 @@ export const LanguageTabs = memo(function LanguageTabs({ isMobile = false }: Lan
6557
onClick={() => setLanguage(lang.id)}
6658
style={{
6759
...baseButtonStyle,
68-
backgroundColor: isActive ? 'var(--theme-memory-lang-active-bg)' : 'var(--theme-memory-lang-inactive-bg)',
60+
backgroundColor: isActive ? 'var(--theme-memory-lang-active-bg)' : 'transparent',
6961
color: isActive ? 'var(--theme-memory-lang-active-text)' : 'var(--theme-memory-lang-inactive-text)',
70-
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,0.1)' : 'none',
62+
borderColor: isActive ? 'var(--theme-memory-reset-border)' : 'transparent',
63+
boxShadow: isActive ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
7164
}}
7265
>
7366
<span
7467
style={{
75-
...indicatorBaseStyle,
68+
width: indicatorSize,
69+
height: indicatorSize,
70+
borderRadius: '50%',
71+
flexShrink: 0,
7672
backgroundColor: isActive ? lang.color : '#d1d5db',
7773
}}
7874
/>

0 commit comments

Comments
 (0)