studio/src/editor/KubikonEditor.module.css
МИН 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

834 lines
18 KiB
CSS
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.

/* ========== KUBIKON EDITOR — тёмный стиль (как Roblox Studio) ==========
Layout: верхняя панель + workspace (slim-left + viewport + slim-right) + status bar.
Тёмная палитра в стиле Roblox Studio: почти-чёрные панели, светлый
текст, синий акцент Рублокса.
*/
.editor {
/* Тёмная основа (как панели Roblox Studio) */
--bg-darkest: #1b1b1b; /* самый тёмный фон рабочей зоны */
--bg-dark: #252525; /* панели */
--bg-mid: #2e2e2e; /* приподнятые блоки */
--bg-light: #383838; /* hover / активные */
/* Акценты Рублокса */
--accent: #4f74ff;
--accent-bright: #6d8aff;
--accent2: #4f74ff;
--wood: #4a4a4a;
--stone: #5a5a5a;
/* Текст */
--text: #e8e8ea;
--text-dim: #9a9a9e;
/* Границы */
--border: #3a3a3a;
/* Семантика */
--danger: #ef4444;
font-family: "Roboto Condensed", system-ui, -apple-system, sans-serif;
display: grid;
grid-template-rows: 56px auto 1fr 28px;
height: 100vh;
width: 100%;
max-width: 100%;
background: var(--bg-darkest);
color: var(--text);
overflow: hidden;
box-sizing: border-box;
font-size: 14px;
}
.editor *,
.editor *::before,
.editor *::after {
box-sizing: border-box;
}
/* === ВЕРХНЯЯ ПАНЕЛЬ === */
.topBar {
display: flex;
align-items: center;
gap: 16px;
padding: 0 16px;
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-darkest) 100%);
border-bottom: 2px solid var(--border);
z-index: 10;
}
.backBtn {
padding: 8px 14px;
background: var(--bg-mid);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.backBtn:hover {
background: var(--bg-light);
border-color: var(--wood);
}
.projectName {
flex: 1;
display: flex;
justify-content: center;
}
.nameInput {
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 6px 12px;
color: var(--text);
font-size: 16px;
text-align: center;
width: 280px;
transition: all 0.15s;
}
.nameInput:hover {
background: var(--bg-mid);
border-color: var(--border);
}
.nameInput:focus {
background: var(--bg-mid);
border-color: var(--accent2);
outline: none;
}
.topActions {
display: flex;
gap: 10px;
align-items: center;
}
.saveStatus {
font-size: 12px;
color: var(--text-dim);
padding: 6px 12px;
border-radius: 4px;
background: var(--bg-mid);
border: 1px solid var(--border);
user-select: none;
transition: all 0.15s;
min-width: 110px;
text-align: center;
}
.saveStatus_saved {
color: var(--accent-bright);
border-color: var(--accent);
}
.saveStatus_saving {
color: var(--accent2);
border-color: var(--wood);
}
.saveStatus_dirty {
color: var(--accent2);
border-color: var(--wood);
}
.saveStatus_error {
color: #f08080;
border-color: var(--danger);
background: rgba(168, 50, 50, 0.15);
}
.toolbarBtn {
padding: 8px 16px;
background: var(--bg-mid);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
font-weight: 600;
}
.toolbarBtn:hover {
background: var(--bg-light);
border-color: var(--accent2);
}
.playBtn {
background: linear-gradient(135deg, #22d97a 0%, #0f9d56 100%);
border-color: transparent;
color: #fff;
box-shadow: 0 6px 16px rgba(34, 217, 122, 0.40);
}
.playBtn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(34, 217, 122, 0.50);
}
.stopBtn {
background: linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%);
border-color: transparent;
color: #fff;
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.40);
}
.stopBtn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(239, 68, 68, 0.50);
}
.playBadge {
position: absolute;
top: 12px;
right: 12px;
background: linear-gradient(135deg, #ec4899 0%, #ef4444 100%);
color: #fff;
padding: 8px 16px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
letter-spacing: 1.5px;
text-transform: uppercase;
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.45);
animation: playBadgePulse 2s ease-in-out infinite;
pointer-events: none;
}
@keyframes playBadgePulse {
0%, 100% { opacity: 0.95; transform: scale(1); }
50% { opacity: 1; transform: scale(1.04); }
}
/* === WORKSPACE === */
.workspace {
display: grid;
grid-template-columns: 240px minmax(0, 1fr) 280px;
overflow: hidden;
min-height: 0;
}
/* === ПАНЕЛИ === */
.leftPanel, .rightPanel {
background: var(--bg-dark);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.leftPanel {
border-right: 2px solid var(--border);
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.rightPanel {
border-left: 2px solid var(--border);
}
.panelTitle {
padding: 12px 16px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-dim);
background: var(--bg-mid);
border-bottom: 1px solid var(--border);
border-top: 1px solid var(--border);
font-weight: 800;
}
.panelTitle:first-child {
border-top: none;
}
/* === ИНСТРУМЕНТЫ === */
.toolList {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 12px;
}
.toolBtn {
aspect-ratio: 1;
background: var(--bg-mid);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 22px;
cursor: pointer;
transition: all 0.12s;
display: flex;
align-items: center;
justify-content: center;
}
.toolBtn:hover {
background: var(--bg-light);
border-color: var(--accent2);
transform: translateY(-1px);
}
.toolActive {
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
border-color: transparent;
box-shadow: 0 6px 16px rgba(51, 87, 255, 0.40);
color: #fff;
transform: translateY(-1px);
}
.toolActive:hover {
background: var(--accent-bright);
border-color: var(--accent-bright);
}
/* === ПАЛИТРА БЛОКОВ === */
.blocksGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: min-content;
align-content: start;
gap: 6px;
padding: 12px;
overflow-y: auto;
flex: 1 1 auto;
min-height: 200px;
}
.blockSwatch {
aspect-ratio: 1;
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
overflow: hidden;
padding: 0;
position: relative;
background: var(--bg-mid);
}
.blockSwatch:hover {
transform: scale(1.08) translateY(-2px);
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 8px 16px rgba(51, 87, 255, 0.20);
z-index: 1;
}
.blockSwatchActive {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent), 0 0 16px rgba(51, 87, 255, 0.45);
transform: scale(1.04);
}
.blockSwatchImg {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
object-fit: cover;
}
.activeBlockInfo {
padding: 10px 16px;
font-size: 12px;
color: var(--text-dim);
background: var(--bg-darkest);
border-top: 1px solid var(--border);
text-align: center;
}
.activeBlockInfo b {
color: var(--accent-bright);
}
.searchBox {
padding: 8px 12px;
background: var(--bg-darkest);
border-bottom: 1px solid var(--border);
}
.searchInput {
width: 100%;
padding: 6px 10px;
background: var(--bg-mid);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 12px;
outline: none;
transition: border-color 0.15s;
}
.searchInput:focus {
border-color: var(--accent2);
}
.searchInput::placeholder {
color: var(--text-dim);
}
.categoryTabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px;
background: var(--bg-darkest);
border-bottom: 1px solid var(--border);
max-height: 110px;
overflow-y: auto;
}
.categoryTab {
padding: 4px 10px;
background: var(--bg-mid);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-dim);
font-size: 11px;
cursor: pointer;
transition: all 0.12s;
white-space: nowrap;
}
.categoryTab:hover {
color: var(--text);
border-color: var(--wood);
}
.categoryTabActive {
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
border-color: transparent;
color: #fff;
box-shadow: 0 4px 10px rgba(51, 87, 255, 0.32);
}
.categoryTabActive:hover {
color: #fff;
border-color: transparent;
transform: translateY(-1px);
}
.blocksEmpty {
grid-column: 1 / -1;
text-align: center;
padding: 20px;
color: var(--text-dim);
font-size: 12px;
}
/* === Палитра-табы (Блоки / Модели) === */
.paletteTabs {
display: flex;
gap: 0;
padding: 0;
background: var(--bg-darkest);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.paletteTab {
flex: 1;
padding: 10px 8px;
background: var(--bg-dark);
border: none;
border-right: 1px solid var(--border);
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
font-weight: 600;
}
.paletteTab:last-child {
border-right: none;
}
.paletteTab:hover {
background: var(--bg-mid);
color: var(--text);
}
.paletteTabActive {
background: var(--bg-mid);
color: var(--accent);
box-shadow: inset 0 -3px 0 var(--accent);
font-weight: 800;
}
.paletteTabActive:hover {
background: var(--bg-mid);
color: var(--accent);
}
/* === Заголовок левой панели ===
* Заменяет старые табы Блоки/Примитивы/Модели — теперь выбор источника
* делается через TopRibbon, а в этой панели мы только показываем
* название активного раздела + иконку. */
.paletteHeader {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-darkest) 100%);
border-bottom: 1px solid var(--border);
user-select: none;
-webkit-user-select: none;
}
.paletteHeaderIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
background: rgba(51, 87, 255, 0.10);
color: var(--accent);
flex-shrink: 0;
}
.paletteHeaderTitle {
font-size: 13px;
font-weight: 700;
color: var(--text);
letter-spacing: 0.1px;
}
/* === Сетка моделей === */
.modelsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: min-content;
align-content: start;
gap: 6px;
padding: 12px;
overflow-y: auto;
flex: 1 1 auto;
min-height: 200px;
}
.modelCard {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 6px;
background: var(--bg-mid);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text);
cursor: pointer;
transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.modelCard:hover {
transform: translateY(-2px);
border-color: var(--accent);
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.10);
}
.modelCardActive {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 6px 16px rgba(51, 87, 255, 0.25);
background: rgba(79,116,255,0.16);
}
.modelIcon {
font-size: 26px;
line-height: 1;
}
.modelName {
font-size: 10px;
text-align: center;
line-height: 1.15;
color: var(--text-dim);
}
.modelCardActive .modelName {
color: var(--text);
}
/* === VIEWPORT === */
.viewport {
position: relative;
background: #0a0e1a;
overflow: hidden;
min-width: 0;
min-height: 0;
height: 100%;
}
.canvas {
width: 100%;
height: 100%;
display: block;
outline: none;
touch-action: none;
}
.viewportHint {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
background: rgba(20, 24, 45, 0.78);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 999px;
padding: 6px 14px;
font-size: 11px;
color: rgba(241, 245, 251, 0.82);
font-weight: 600;
letter-spacing: 0.2px;
pointer-events: none;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.30);
}
/* === HIERARCHY === */
.hierarchy {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.hierarchyItem {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
cursor: pointer;
color: var(--text);
font-size: 13px;
transition: background 0.12s;
}
.hierarchyItem:hover {
background: var(--bg-mid);
}
/* === INSPECTOR === */
.inspector {
flex: 1;
padding: 12px 16px;
overflow-y: auto;
}
.inspectorEmpty {
color: var(--text-dim);
font-size: 13px;
text-align: center;
padding: 24px 8px;
line-height: 1.5;
border: 1px dashed var(--border);
border-radius: 6px;
}
/* === STATUS BAR === */
.statusBar {
display: flex;
align-items: center;
gap: 24px;
padding: 0 16px;
background: var(--bg-darkest);
border-top: 1px solid var(--border);
color: var(--text-dim);
font-size: 11px;
letter-spacing: 0.5px;
}
.statusItem {
display: flex;
align-items: center;
gap: 6px;
}
/* === АДАПТИВ === */
@media (max-width: 1100px) {
.workspace {
grid-template-columns: 200px minmax(0, 1fr) 240px;
}
}
@media (max-width: 800px) {
.workspace {
grid-template-columns: 60px minmax(0, 1fr);
}
.rightPanel {
display: none;
}
.blocksGrid {
grid-template-columns: repeat(2, 1fr);
}
}
/* === Тулбокс-вкладка моделей === */
.modelsTabContent {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
flex: 1 1 auto;
overflow-y: auto;
}
.toolboxButton {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
border: 1px solid transparent;
border-radius: 12px;
color: #fff;
cursor: pointer;
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
width: 100%;
text-align: left;
box-shadow: 0 8px 20px rgba(51, 87, 255, 0.32);
font-family: inherit;
}
.toolboxButton:hover {
transform: translateY(-3px);
box-shadow: 0 14px 28px rgba(51, 87, 255, 0.45);
}
.toolboxButtonIcon {
font-size: 36px;
line-height: 1;
}
.toolboxButtonText {
flex: 1;
}
.toolboxButtonTitle {
font-size: 14px;
font-weight: 700;
line-height: 1.2;
}
.toolboxButtonSub {
font-size: 11px;
opacity: 0.85;
margin-top: 2px;
}
.activeModelCard {
background: rgba(79,116,255,0.16);
border: 1px solid var(--accent);
border-radius: 12px;
padding: 12px 14px;
box-shadow: 0 4px 12px rgba(51, 87, 255, 0.10);
}
.activeModelLabel {
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 4px;
}
.activeModelName {
font-size: 13px;
color: var(--text);
font-weight: 600;
}
.activeModelCat {
font-size: 11px;
color: var(--text-dim);
margin-top: 2px;
}
.activeModelChangeBtn {
margin-top: 10px;
width: 100%;
padding: 10px;
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
color: #fff;
border: 1px solid transparent;
border-radius: 10px;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: all 200ms ease;
box-shadow: 0 4px 12px rgba(51, 87, 255, 0.30);
font-family: inherit;
}
.activeModelChangeBtn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(51, 87, 255, 0.45);
}
/* === История недавно использованных моделей === */
.recentModelsBlock {
background: rgba(79,116,255,0.16);
border: 1px solid var(--accent);
border-radius: 14px;
padding: 14px 12px 16px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(51, 87, 255, 0.08);
}
.recentModelsGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 10px;
}
.recentThumbCard {
aspect-ratio: 1 / 1;
background: var(--bg-mid);
border: 1px solid var(--border);
border-radius: 10px;
padding: 6px;
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
min-height: 64px;
}
.recentThumbCard:hover {
border-color: var(--accent);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 8px 16px rgba(51, 87, 255, 0.22);
}
.recentThumbCardActive {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent), 0 6px 16px rgba(51, 87, 255, 0.35);
transform: scale(1.04);
}
.recentThumbImg {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.recentThumbIcon {
font-size: 24px;
line-height: 1;
}