online-compiler-widget 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FileStorage/obj/FileStorage.csproj.EntityFrameworkCore.targets +28 -0
- package/README.md +1 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/openapitools.json +7 -0
- package/package.json +36 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/vite.svg +1 -0
- package/src/App.css +49 -0
- package/src/App.tsx +84 -0
- package/src/api/.openapi-generator/FILES +25 -0
- package/src/api/.openapi-generator/VERSION +1 -0
- package/src/api/.openapi-generator-ignore +23 -0
- package/src/api/api.ts +1312 -0
- package/src/api/base.ts +62 -0
- package/src/api/common.ts +113 -0
- package/src/api/configuration.ts +121 -0
- package/src/api/docs/CompilationError.md +26 -0
- package/src/api/docs/CompileRequest.md +22 -0
- package/src/api/docs/CompileResult.md +28 -0
- package/src/api/docs/CompilerApi.md +263 -0
- package/src/api/docs/CreateFileDto.md +22 -0
- package/src/api/docs/CreateProjectRequest.md +20 -0
- package/src/api/docs/FileApi.md +274 -0
- package/src/api/docs/ProcessStatus.md +28 -0
- package/src/api/docs/ProjectApi.md +362 -0
- package/src/api/docs/ProjectInfo.md +24 -0
- package/src/api/docs/ProjectStats.md +28 -0
- package/src/api/docs/RenameFileDto.md +20 -0
- package/src/api/docs/RenameProjectRequest.md +20 -0
- package/src/api/docs/RunRequest.md +24 -0
- package/src/api/docs/RunResult.md +30 -0
- package/src/api/docs/RunningProjectInfo.md +26 -0
- package/src/api/docs/UpdateFileDto.md +20 -0
- package/src/api/git_push.sh +57 -0
- package/src/api/index.ts +18 -0
- package/src/assets/Badge.svg +17 -0
- package/src/assets/closeIcon.svg +20 -0
- package/src/assets/documentIcon.svg +11 -0
- package/src/assets/history.svg +11 -0
- package/src/assets/output.svg +12 -0
- package/src/assets/plus.svg +20 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/save-icon.svg +11 -0
- package/src/assets/shield.svg +10 -0
- package/src/assets/start.svg +11 -0
- package/src/assets/stop.svg +11 -0
- package/src/components/CompilerWidget.module.scss +169 -0
- package/src/components/CompilerWidget.tsx +279 -0
- package/src/components/FileExplorer.module.scss +372 -0
- package/src/components/FileExplorer.tsx +285 -0
- package/src/components/MonacoEditorWrapper.module.scss +29 -0
- package/src/components/MonacoEditorWrapper.tsx +74 -0
- package/src/components/OutputPanel.module.scss +123 -0
- package/src/components/OutputPanel.tsx +53 -0
- package/src/components/RunContainer.module.scss +150 -0
- package/src/components/RunContainer.tsx +34 -0
- package/src/hooks/useCompiler.ts +228 -0
- package/src/hooks/useInitialNodes.ts +0 -0
- package/src/index.css +78 -0
- package/src/main.tsx +7 -0
- package/src/types/EditorDocument.ts +8 -0
- package/swagger.json +1020 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import cls from "./FileExplorer.module.scss";
|
|
3
|
+
import DocumentIcon from "../assets/documentIcon.svg?react";
|
|
4
|
+
import PlusIcon from "../assets/plus.svg?react";
|
|
5
|
+
import type { EditorDocument } from "../types/EditorDocument";
|
|
6
|
+
|
|
7
|
+
interface FileExplorerProps {
|
|
8
|
+
documents: EditorDocument[];
|
|
9
|
+
selectedId: string | null;
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
onAdd: (fileName?: string) => void;
|
|
12
|
+
onRename: (id: string, newName: string) => void;
|
|
13
|
+
onDelete: (id: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Компонент модального окна для создания файла
|
|
17
|
+
interface AddFileModalProps {
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
onConfirm: (fileName: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AddFileModal: React.FC<AddFileModalProps> = ({ isOpen, onClose, onConfirm }) => {
|
|
24
|
+
const [fileName, setFileName] = useState("");
|
|
25
|
+
const [error, setError] = useState("");
|
|
26
|
+
|
|
27
|
+
if (!isOpen) return null;
|
|
28
|
+
|
|
29
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
|
|
32
|
+
const trimmedName = fileName.trim();
|
|
33
|
+
|
|
34
|
+
if (!trimmedName) {
|
|
35
|
+
setError("Введите имя файла");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Проверяем расширение
|
|
40
|
+
if (!trimmedName.includes('.')) {
|
|
41
|
+
setError("Добавьте расширение файла (например, .cs, .js, .txt)");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Проверяем допустимые расширения
|
|
46
|
+
const validExtensions = ['.cs', '.js', '.txt'];
|
|
47
|
+
const hasValidExtension = validExtensions.some(ext => trimmedName.endsWith(ext));
|
|
48
|
+
|
|
49
|
+
if (!hasValidExtension) {
|
|
50
|
+
setError(`Используйте одно из расширений: ${validExtensions.join(', ')}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onConfirm(trimmedName);
|
|
55
|
+
setFileName("");
|
|
56
|
+
setError("");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleClose = () => {
|
|
60
|
+
setFileName("");
|
|
61
|
+
setError("");
|
|
62
|
+
onClose();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={cls.modalOverlay} onClick={handleClose}>
|
|
67
|
+
<div className={cls.modalContent} onClick={(e) => e.stopPropagation()}>
|
|
68
|
+
<h3>Создать новый файл</h3>
|
|
69
|
+
<form onSubmit={handleSubmit}>
|
|
70
|
+
<div className={cls.formGroup}>
|
|
71
|
+
<label htmlFor="fileName">Имя файла с расширением:</label>
|
|
72
|
+
<input
|
|
73
|
+
id="fileName"
|
|
74
|
+
type="text"
|
|
75
|
+
value={fileName}
|
|
76
|
+
onChange={(e) => {
|
|
77
|
+
setFileName(e.target.value);
|
|
78
|
+
setError("");
|
|
79
|
+
}}
|
|
80
|
+
placeholder="Например: program.cs или script.js"
|
|
81
|
+
autoFocus
|
|
82
|
+
className={error ? cls.error : ""}
|
|
83
|
+
/>
|
|
84
|
+
<div className={cls.helpText}>
|
|
85
|
+
Введите имя файла с расширением (.cs, .js, .txt)
|
|
86
|
+
</div>
|
|
87
|
+
{error && <div className={cls.errorMessage}>{error}</div>}
|
|
88
|
+
</div>
|
|
89
|
+
<div className={cls.modalActions}>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={handleClose}
|
|
93
|
+
className={cls.cancelBtn}
|
|
94
|
+
>
|
|
95
|
+
Отмена
|
|
96
|
+
</button>
|
|
97
|
+
<button
|
|
98
|
+
type="submit"
|
|
99
|
+
className={cls.confirmBtn}
|
|
100
|
+
disabled={!fileName.trim()}
|
|
101
|
+
>
|
|
102
|
+
Создать
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</form>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const FileExplorer: React.FC<FileExplorerProps> = ({
|
|
112
|
+
documents,
|
|
113
|
+
selectedId,
|
|
114
|
+
onSelect,
|
|
115
|
+
onAdd,
|
|
116
|
+
onRename,
|
|
117
|
+
onDelete
|
|
118
|
+
}) => {
|
|
119
|
+
const [menuId, setMenuId] = useState<string | null>(null);
|
|
120
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
121
|
+
const [editName, setEditName] = useState("");
|
|
122
|
+
const [showAddModal, setShowAddModal] = useState(false);
|
|
123
|
+
|
|
124
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
125
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
126
|
+
|
|
127
|
+
// Закрытие меню при клике вне
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const handler = (e: MouseEvent) => {
|
|
130
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
131
|
+
setMenuId(null);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
document.addEventListener("mousedown", handler);
|
|
136
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Фокус на input при начале редактирования
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (editingId && inputRef.current) {
|
|
142
|
+
inputRef.current.focus();
|
|
143
|
+
inputRef.current.select();
|
|
144
|
+
}
|
|
145
|
+
}, [editingId]);
|
|
146
|
+
|
|
147
|
+
const handleStartRename = useCallback((id: string, currentName: string) => {
|
|
148
|
+
setMenuId(null);
|
|
149
|
+
setEditingId(id);
|
|
150
|
+
setEditName(currentName);
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
const handleSaveRename = useCallback((id: string) => {
|
|
154
|
+
if (editName.trim() && editName.trim() !== documents.find(d => d.id === id)?.name) {
|
|
155
|
+
onRename(id, editName.trim());
|
|
156
|
+
}
|
|
157
|
+
setEditingId(null);
|
|
158
|
+
setEditName("");
|
|
159
|
+
}, [editName, onRename, documents]);
|
|
160
|
+
|
|
161
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent, id: string) => {
|
|
162
|
+
if (e.key === 'Enter') {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
handleSaveRename(id);
|
|
165
|
+
} else if (e.key === 'Escape') {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
setEditingId(null);
|
|
168
|
+
setEditName("");
|
|
169
|
+
}
|
|
170
|
+
}, [handleSaveRename]);
|
|
171
|
+
|
|
172
|
+
const handleBlur = useCallback((id: string) => {
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
handleSaveRename(id);
|
|
175
|
+
}, 100);
|
|
176
|
+
}, [handleSaveRename]);
|
|
177
|
+
|
|
178
|
+
const handleAddFile = (fileName: string) => {
|
|
179
|
+
onAdd(fileName);
|
|
180
|
+
setShowAddModal(false);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className={cls.fileExplorer}>
|
|
185
|
+
<AddFileModal
|
|
186
|
+
isOpen={showAddModal}
|
|
187
|
+
onClose={() => setShowAddModal(false)}
|
|
188
|
+
onConfirm={handleAddFile}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
<div className={cls.header}>
|
|
192
|
+
<DocumentIcon className={cls.documentIcon} />
|
|
193
|
+
<span>Файлы</span>
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => setShowAddModal(true)}
|
|
196
|
+
className={cls.addButton}
|
|
197
|
+
title="Добавить файл"
|
|
198
|
+
>
|
|
199
|
+
<PlusIcon className={cls.plusIcon} />
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<ul className={cls.fileList}>
|
|
204
|
+
{documents.map(doc => (
|
|
205
|
+
<li
|
|
206
|
+
key={doc.id}
|
|
207
|
+
className={`${cls.fileItem} ${
|
|
208
|
+
selectedId === doc.id ? cls.selected : ""
|
|
209
|
+
} ${doc.modified ? cls.modified : ""}`}
|
|
210
|
+
onClick={() => !editingId && onSelect(doc.id)}
|
|
211
|
+
>
|
|
212
|
+
<div className={cls.clickZone}>
|
|
213
|
+
<span className={cls.fileIcon}>📄</span>
|
|
214
|
+
|
|
215
|
+
{editingId === doc.id ? (
|
|
216
|
+
<div className={cls.editContainer}>
|
|
217
|
+
<input
|
|
218
|
+
ref={inputRef}
|
|
219
|
+
type="text"
|
|
220
|
+
className={cls.editInput}
|
|
221
|
+
value={editName}
|
|
222
|
+
onChange={(e) => setEditName(e.target.value)}
|
|
223
|
+
onKeyDown={(e) => handleKeyDown(e, doc.id)}
|
|
224
|
+
onBlur={() => handleBlur(doc.id)}
|
|
225
|
+
onClick={(e) => e.stopPropagation()}
|
|
226
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
<div className={cls.nameContainer}>
|
|
231
|
+
<span className={cls.itemText}>{doc.name}</span>
|
|
232
|
+
{doc.modified && (
|
|
233
|
+
<span
|
|
234
|
+
className={cls.modifiedDot}
|
|
235
|
+
title="Файл изменен (не сохранен)"
|
|
236
|
+
>
|
|
237
|
+
●
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{editingId !== doc.id && (
|
|
245
|
+
<button
|
|
246
|
+
className={cls.moreBtn}
|
|
247
|
+
onClick={(e) => {
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
setMenuId(menuId === doc.id ? null : doc.id);
|
|
250
|
+
}}
|
|
251
|
+
title="Действия с файлом"
|
|
252
|
+
>
|
|
253
|
+
⋮
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{menuId === doc.id && (
|
|
258
|
+
<div className={cls.contextMenu} ref={menuRef}>
|
|
259
|
+
<div
|
|
260
|
+
className={cls.menuItem}
|
|
261
|
+
onClick={(e) => {
|
|
262
|
+
e.stopPropagation();
|
|
263
|
+
handleStartRename(doc.id, doc.name);
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
Переименовать
|
|
267
|
+
</div>
|
|
268
|
+
<div
|
|
269
|
+
className={cls.menuItem}
|
|
270
|
+
onClick={(e) => {
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
setMenuId(null);
|
|
273
|
+
onDelete(doc.id);
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
Удалить
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</li>
|
|
281
|
+
))}
|
|
282
|
+
</ul>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
$editor-header-bg: #f9fafb;
|
|
2
|
+
$editor-border: #ddd;
|
|
3
|
+
|
|
4
|
+
.editorContainer {
|
|
5
|
+
flex: 1 1 auto;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
|
|
11
|
+
.editorHeader {
|
|
12
|
+
flex: 0 0 32px;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
padding: 0 12px;
|
|
16
|
+
background: $editor-header-bg;
|
|
17
|
+
border-bottom: 1px solid #eee;
|
|
18
|
+
font-weight: 500;
|
|
19
|
+
font-size: 13px;
|
|
20
|
+
color: #111;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.monaco-editor {
|
|
24
|
+
flex: 1 1 auto;
|
|
25
|
+
overflow: hidden !important;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/components/MonacoEditorWrapper.tsx
|
|
2
|
+
import React, { useRef, useEffect } from "react";
|
|
3
|
+
import Editor from "@monaco-editor/react";
|
|
4
|
+
import * as monaco from "monaco-editor";
|
|
5
|
+
import cls from "./MonacoEditorWrapper.module.scss";
|
|
6
|
+
import StartIcon from "../assets/start.svg?react";
|
|
7
|
+
import StopIcon from "../assets/stop.svg?react";
|
|
8
|
+
|
|
9
|
+
interface MonacoEditorWrapperProps {
|
|
10
|
+
code: string;
|
|
11
|
+
language: string; // "javascript" | "csharp"
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
theme?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MonacoEditorWrapper: React.FC<MonacoEditorWrapperProps> = ({
|
|
17
|
+
code,
|
|
18
|
+
language,
|
|
19
|
+
onChange,
|
|
20
|
+
theme = "vs-light",
|
|
21
|
+
}) => {
|
|
22
|
+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
23
|
+
const modelRef = useRef<monaco.editor.ITextModel | null>(null);
|
|
24
|
+
|
|
25
|
+
// Создаём модель при загрузке редактора
|
|
26
|
+
const handleEditorDidMount = (
|
|
27
|
+
editor: monaco.editor.IStandaloneCodeEditor
|
|
28
|
+
) => {
|
|
29
|
+
editorRef.current = editor;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 🔥 Создание / обновление модели при переключении документа
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!editorRef.current) return;
|
|
35
|
+
|
|
36
|
+
// удаляем старую модель
|
|
37
|
+
if (modelRef.current) {
|
|
38
|
+
modelRef.current?.dispose();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const newModel = monaco.editor.createModel(code, language);
|
|
42
|
+
modelRef.current = newModel;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
editorRef.current?.focus();
|
|
46
|
+
}, [code, language]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cls.editorContainer}>
|
|
50
|
+
<div className={cls.editorHeader}>
|
|
51
|
+
<span>{language === "csharp" ? "C#" : "JS"}</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<Editor
|
|
55
|
+
height="100%"
|
|
56
|
+
language={language}
|
|
57
|
+
theme={theme}
|
|
58
|
+
onMount={handleEditorDidMount}
|
|
59
|
+
value={code} // контролируемое значение
|
|
60
|
+
onChange={(v) => onChange(v || "")}
|
|
61
|
+
options={{
|
|
62
|
+
minimap: { enabled: false },
|
|
63
|
+
fontSize: 10,
|
|
64
|
+
scrollBeyondLastLine: false,
|
|
65
|
+
wordWrap: "off",
|
|
66
|
+
lineNumbers: "on",
|
|
67
|
+
folding: true,
|
|
68
|
+
renderLineHighlight: "all",
|
|
69
|
+
tabSize: 4,
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
$output-bg: #fafafa;
|
|
2
|
+
$output-border: #ddd;
|
|
3
|
+
$tab-active-bg: #e0f0ff;
|
|
4
|
+
$tab-active-border: #007bff;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
.outputPanel {
|
|
9
|
+
flex: 0 0 300px; // ширина правой панели (можно менять через resize)
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
height: 100%;
|
|
13
|
+
background: $output-bg;
|
|
14
|
+
border-left: 1px solid $output-border;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
|
|
17
|
+
.tabs {
|
|
18
|
+
display: flex;
|
|
19
|
+
background: #F8FAFC;
|
|
20
|
+
border-bottom: 1px solid #E2E8F0;
|
|
21
|
+
|
|
22
|
+
.tab {
|
|
23
|
+
width: 100%;
|
|
24
|
+
padding: 7px 12px;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
font-size: 8px;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
border: none;
|
|
30
|
+
transition: background 0.2s;
|
|
31
|
+
border-radius: 0px;
|
|
32
|
+
outline: none;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
|
|
36
|
+
&.active {
|
|
37
|
+
border: 0px;
|
|
38
|
+
background: #FFFFFF;
|
|
39
|
+
border-bottom: 1px solid $tab-active-border;
|
|
40
|
+
border-radius: 0px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.icon {
|
|
44
|
+
margin-right: 2px;
|
|
45
|
+
height: 10px;
|
|
46
|
+
padding-top: 2px;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.content {
|
|
52
|
+
flex: 1 1 auto;
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
padding: 8px 12px;
|
|
55
|
+
|
|
56
|
+
background: #fff;
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
|
|
59
|
+
pre {
|
|
60
|
+
background: #f8f8f8;
|
|
61
|
+
padding: 8px;
|
|
62
|
+
|
|
63
|
+
font-family: 'Courier New', Courier, monospace;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
line-height: 1.4;
|
|
66
|
+
white-space: pre-wrap;
|
|
67
|
+
word-wrap: break-word;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.output {
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
white-space: pre-wrap;
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
line-height: 20px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.placeholder {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
height: 100%;
|
|
84
|
+
color: #999;
|
|
85
|
+
text-align: center;
|
|
86
|
+
gap: 6px;
|
|
87
|
+
|
|
88
|
+
.bigIcon{
|
|
89
|
+
height: 28px;
|
|
90
|
+
color: #CAD5E2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
span {
|
|
94
|
+
font-size: 28px;
|
|
95
|
+
opacity: 0.4;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
p {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font-family: Arial;
|
|
101
|
+
font-weight: 400;
|
|
102
|
+
font-size: 11px;
|
|
103
|
+
color: #62748E;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
small {
|
|
107
|
+
font-family: Arial;
|
|
108
|
+
font-weight: 400;
|
|
109
|
+
font-size: 9px;
|
|
110
|
+
opacity: 0.7;
|
|
111
|
+
color: #90A1B9;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.historyItem {
|
|
116
|
+
margin-bottom: 8px;
|
|
117
|
+
padding: 6px;
|
|
118
|
+
background: #f0f0f0;
|
|
119
|
+
|
|
120
|
+
border-left: 3px solid $tab-active-border;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/components/OutputPanel.tsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import cls from './OutputPanel.module.scss';
|
|
4
|
+
import HistoryIcon from "../assets/history.svg?react";
|
|
5
|
+
import OutputIcon from "../assets/output.svg?react";
|
|
6
|
+
|
|
7
|
+
interface OutputPanelProps {
|
|
8
|
+
output: string;
|
|
9
|
+
history: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const OutputPanel: React.FC<OutputPanelProps> = ({ output, history }) => {
|
|
13
|
+
const [activeTab, setActiveTab] = useState<'output' | 'history'>('output');
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={cls.outputPanel}>
|
|
17
|
+
<div className={cls.tabs}>
|
|
18
|
+
<button
|
|
19
|
+
className={`${cls.tab} ${activeTab === 'output' ? cls.active : ''}`}
|
|
20
|
+
onClick={() => setActiveTab('output')}
|
|
21
|
+
>
|
|
22
|
+
<OutputIcon className={cls.icon}/>
|
|
23
|
+
<span>Вывод</span>
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
className={`${cls.tab} ${activeTab === 'history' ? cls.active : ''}`}
|
|
27
|
+
onClick={() => setActiveTab('history')}
|
|
28
|
+
>
|
|
29
|
+
<HistoryIcon className={cls.icon}/>
|
|
30
|
+
<span>История</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className={cls.content}>
|
|
35
|
+
{!output && activeTab === 'output' && (
|
|
36
|
+
<div className={cls.placeholder}>
|
|
37
|
+
<OutputIcon className={cls.bigIcon}/>
|
|
38
|
+
<p>Нет результата кода</p>
|
|
39
|
+
<small>Запустите свой код, чтобы увидеть результат</small>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
<p className={cls.output}>{output}</p>
|
|
44
|
+
|
|
45
|
+
{activeTab === 'history' && (
|
|
46
|
+
<div className={cls.placeholder}>
|
|
47
|
+
<HistoryIcon className={cls.bigIcon}/>
|
|
48
|
+
<p>История пуста</p>
|
|
49
|
+
</div>)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|