studio/src/editor/SceneTabs.jsx
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
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)
2026-05-27 23:41:10 +03:00

130 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;