Open-source веб-студия для создания игр Рублокса, двойная лицензия AGPL-3.0 + Коммерческая. Главное: - Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16 - Самодостаточный движок ~28к строк (66 файлов): BlockManager, TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов - Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco) - Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn) - Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt) - 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.) - Конфигурируемый бэкенд через VITE_API_BASE — работает со staging (dev-api.rublox.pro) без настройки - Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка - Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG - ESLint + Prettier + EditorConfig - Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md - Issue templates: bug_report, feature_request, security_disclosure Перед публикацией: - Все импорты из minecraftia заменены на локальные - Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env - Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо) - AdminKubikonModeration не публикуется (модерация — в team.rublox.pro) - 93 МБ ассетов public/kubikon-assets вынесены в .gitignore (раздаются через release artifact)
130 lines
6.0 KiB
JavaScript
130 lines
6.0 KiB
JavaScript
import React from 'react';
|
||
import Icon from './Icon';
|
||
|
||
/**
|
||
* SceneTabs — полоска табов над viewport (синий wow-стиль Рублокса).
|
||
*
|
||
* Один статичный таб «🎬 Сцена» (всегда первый, не закрывается) +
|
||
* динамические табы открытых скриптов. Активный таб подсвечен.
|
||
*
|
||
* Props:
|
||
* tabs — массив { id: 'scene' | scriptId, kind: 'scene' | 'script', title }
|
||
* activeId — id активного таба
|
||
* onSelect(id)
|
||
* onClose(id) — для табов скриптов
|
||
*/
|
||
const SceneTabs = ({ tabs = [], activeId, onSelect, onClose }) => {
|
||
return (
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'flex-end',
|
||
gap: 4,
|
||
padding: '8px 12px 0',
|
||
background: '#1b1b1b',
|
||
borderBottom: '1px solid #3a3a3a',
|
||
minHeight: 42,
|
||
fontFamily: '"Roboto Condensed", system-ui, -apple-system, sans-serif',
|
||
}}>
|
||
{tabs.map(t => {
|
||
const active = t.id === activeId;
|
||
return (
|
||
<div
|
||
key={t.id}
|
||
onClick={() => onSelect?.(t.id)}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '8px 14px 10px',
|
||
background: active ? '#2e2e2e' : 'transparent',
|
||
color: active ? '#6d8aff' : '#9a9a9e',
|
||
borderTopLeftRadius: 12,
|
||
borderTopRightRadius: 12,
|
||
border: '1px solid',
|
||
borderColor: active ? '#3a3a3a' : 'transparent',
|
||
borderBottomColor: active ? '#2e2e2e' : 'transparent',
|
||
marginBottom: -1,
|
||
cursor: 'pointer',
|
||
fontSize: 13,
|
||
fontWeight: active ? 800 : 600,
|
||
letterSpacing: 0.1,
|
||
transition: 'all 200ms ease',
|
||
position: 'relative',
|
||
top: active ? 0 : 1,
|
||
boxShadow: active ? '0 -2px 6px rgba(0, 0, 0, 0.3)' : 'none',
|
||
}}
|
||
title={t.title}
|
||
onMouseEnter={(e) => {
|
||
if (!active) {
|
||
e.currentTarget.style.background = '#2e2e2e';
|
||
e.currentTarget.style.color = '#e8e8ea';
|
||
}
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
if (!active) {
|
||
e.currentTarget.style.background = 'transparent';
|
||
e.currentTarget.style.color = '#9a9a9e';
|
||
}
|
||
}}
|
||
>
|
||
{/* Маленькая активная полоска сверху таба */}
|
||
{active && (
|
||
<div style={{
|
||
position: 'absolute', left: 12, right: 12, top: 0, height: 3,
|
||
background: 'linear-gradient(90deg, #4f74ff 0%, #3a57d8 100%)',
|
||
borderRadius: '0 0 3px 3px',
|
||
}} />
|
||
)}
|
||
<span>
|
||
{t.kind === 'scene'
|
||
? <Icon name="image" size={14} />
|
||
: <Icon name="script" size={14} />}
|
||
</span>
|
||
<span style={{
|
||
maxWidth: 160, overflow: 'hidden',
|
||
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||
}}>
|
||
{t.title}
|
||
</span>
|
||
{t.kind === 'script' && (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); onClose?.(t.id); }}
|
||
title="Закрыть таб"
|
||
style={{
|
||
background: 'transparent',
|
||
border: 'none',
|
||
color: 'inherit',
|
||
cursor: 'pointer',
|
||
fontSize: 14,
|
||
fontWeight: 700,
|
||
lineHeight: 1,
|
||
padding: '2px 6px',
|
||
marginLeft: 2,
|
||
opacity: 0.55,
|
||
borderRadius: 6,
|
||
transition: 'all 150ms ease',
|
||
fontFamily: 'inherit',
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.opacity = '1';
|
||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.2)';
|
||
e.currentTarget.style.color = '#ff6b6b';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.opacity = '0.55';
|
||
e.currentTarget.style.background = 'transparent';
|
||
e.currentTarget.style.color = 'inherit';
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SceneTabs;
|