Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026). Содержит изменения которые делались в процессе подготовки прод-окружения: Фиксы импортов после выноса из minecraftia: - Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/) - Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/ - API.js скопирован из минки целиком (было 8 экспортов, стало 312) - Добавлены PLAYER_URL, MyButton_1, недостающие компоненты - Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require) Структура ассетов: - public/kubikon-templates/ → public/assets/kubikon-templates/ - public/kubikon-learn/ → public/assets/kubikon-learn/ - (код искал в /assets/, файлы лежали без /assets/) Навигация роутов внутри студии: - /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced) - /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X UI: - Новый компонент StudioHeader (61px, как в минке) + копия favicon - WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера - SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке) - Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык) Документация: - docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR - docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API - API_USAGE.md — список эндпоинтов backend - README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/ .gitignore: - public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
469 lines
18 KiB
JavaScript
469 lines
18 KiB
JavaScript
import React, { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import cl from './KubikonStudio.module.css';
|
||
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
|
||
import useDeviceType from '../hooks/useDeviceType';
|
||
import KubikonDesktopOnlyStub from './KubikonDesktopOnlyStub';
|
||
import Icon from '../editor/Icon';
|
||
|
||
/**
|
||
* KubikonHeroKit — кит из 10 вариантов hero-блока для главной Studio.
|
||
* URL: /hero-kit
|
||
*
|
||
* В оригинальном Roblox Studio на главной — крупное изображение игры.
|
||
* Здесь 10 вариантов такого hero-блока: каждый — реальный чистый
|
||
* скриншот игры Рублокса (снят dev-tools/wiki-shots/shoot-hero.js,
|
||
* 1920×800, без HUD) + своя композиция текста/кнопки/оверлея.
|
||
*
|
||
* Пользователь смотрит кит, выбирает номер — выбранный вариант
|
||
* переносится на главную Studio (секция HERO в KubikonStudio.jsx).
|
||
*
|
||
* Картинки: public/wiki/hero/hero-<id>.png
|
||
*/
|
||
|
||
// ── 10 вариантов hero-блока ─────────────────────────────────────────
|
||
// img — файл кадра из public/wiki/hero/
|
||
// title — крупный заголовок
|
||
// desc — подзаголовок
|
||
// btn — текст кнопки
|
||
// align — 'left' | 'center' — расположение текстового блока
|
||
// overlay — тип затемнения под текстом для читаемости:
|
||
// 'left-dark' (градиент слева), 'bottom-dark' (снизу),
|
||
// 'full-soft' (лёгкое сплошное), 'panel' (карточка-плашка)
|
||
// theme — 'light' (белый текст) — все кадры тёмные/яркие
|
||
const VARIANTS = [
|
||
{
|
||
id: 1, img: 'hero-1.png', align: 'left', overlay: 'left-dark',
|
||
title: 'Создай целый мир',
|
||
desc: 'Холмы, леса, реки — построй свой 3D-мир и пригласи друзей. Всё начинается с пустого холста.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
{
|
||
id: 2, img: 'hero-79.png', align: 'left', overlay: 'left-dark',
|
||
title: 'Твоя игра — твои правила',
|
||
desc: 'Паркур, корабли, порталы, радуги. Собери игру мечты и опубликуй её в ленте Рублокса.',
|
||
btn: 'Начать создавать',
|
||
},
|
||
{
|
||
id: 3, img: 'hero-82.png', align: 'center', overlay: 'full-soft',
|
||
title: 'Построй. Запусти. Поделись.',
|
||
desc: 'От первого кубика до целой игры с порталами и механиками — в одном редакторе.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
{
|
||
id: 4, img: 'hero-87.png', align: 'left', overlay: 'bottom-dark',
|
||
title: 'Приключения ждут',
|
||
desc: 'Лава, обрывы, ловушки — придумай уровень, который не пройти с первого раза.',
|
||
btn: 'Создать игру',
|
||
},
|
||
{
|
||
id: 5, img: 'hero-226.png', align: 'left', overlay: 'left-dark',
|
||
title: 'Создавай большие игры',
|
||
desc: 'Замки, подземелья, целые королевства. Редактор Рублокса справится с любой задумкой.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
{
|
||
id: 6, img: 'hero-86.png', align: 'panel', overlay: 'panel',
|
||
title: 'Сделай свою игру',
|
||
desc: 'Выбери шаблон или начни с нуля. Опубликуй — и в неё будут играть тысячи.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
{
|
||
id: 7, img: 'hero-62.png', align: 'center', overlay: 'full-soft',
|
||
title: 'Парящие острова и не только',
|
||
desc: 'Размести блоки в воздухе, собери паркур, добавь скрипты — всё возможно.',
|
||
btn: 'Начать',
|
||
},
|
||
{
|
||
id: 8, img: 'hero-80.png', align: 'left', overlay: 'left-dark',
|
||
title: 'Выше облаков',
|
||
desc: 'Построй небесный мир и проложи маршрут среди облаков. Твоя фантазия — без границ.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
{
|
||
id: 9, img: 'hero-84.png', align: 'left', overlay: 'bottom-dark',
|
||
title: 'Тайны подземелий',
|
||
desc: 'Тёмные коридоры, секретные комнаты, ловушки. Создай игру, в которой страшно и интересно.',
|
||
btn: 'Создать игру',
|
||
},
|
||
{
|
||
id: 10, img: 'hero-45.png', align: 'panel', overlay: 'panel',
|
||
title: 'Арена для твоих игр',
|
||
desc: 'Гонки, битвы, испытания на время. Собери арену и зови друзей соревноваться.',
|
||
btn: '+ Новая игра',
|
||
},
|
||
];
|
||
|
||
// Один hero-вариант — рендерит готовый блок ровно так, как он
|
||
// будет выглядеть на главной Studio.
|
||
const HeroPreview = ({ v }) => {
|
||
const overlayCls = `hkOverlay hkOverlay--${v.overlay}`;
|
||
return (
|
||
<div className="hkHero">
|
||
<img className="hkHeroImg" src={`/wiki/hero/${v.img}`} alt={v.title} />
|
||
<div className={overlayCls} />
|
||
<div className={`hkHeroInner hkHeroInner--${v.align}`}>
|
||
<div className="hkHeroBox">
|
||
<div className="hkBadge">NEW</div>
|
||
<h2 className="hkTitle">{v.title}</h2>
|
||
<p className="hkDesc">{v.desc}</p>
|
||
<span className="hkBtn">{v.btn}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const KubikonHeroKit = () => {
|
||
const navigate = useNavigate();
|
||
const { isDesktop } = useDeviceType();
|
||
const [chosen, setChosen] = useState(null);
|
||
|
||
if (!isDesktop) {
|
||
return <KubikonDesktopOnlyStub feature="Кит hero-блоков Рублокс" />;
|
||
}
|
||
|
||
return (
|
||
<div className={cl.studio}>
|
||
<style>{INLINE_STYLES}</style>
|
||
|
||
{/* === Левая боковая панель === */}
|
||
<aside className={cl.sidebar}>
|
||
<div className={cl.sidebarHeader}>
|
||
<div className={cl.sidebarLogo}>
|
||
<RublocsLogo size={44} bg="#16181d" />
|
||
</div>
|
||
<div className={cl.sidebarTitle}>
|
||
<div className={cl.brandName}>Рублокс</div>
|
||
<div className={cl.brandSub}>Студия</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav className={cl.sidebarNav}>
|
||
<button
|
||
className={cl.navNewGame}
|
||
onClick={() => navigate('/edit/new')}
|
||
>
|
||
<span className={cl.navNewGameIco}><Icon name="plus" size={15} /></span>
|
||
<span>Новая игра</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=recent')}>
|
||
<span className={cl.navIcon}><Icon name="clock" size={15} /></span>
|
||
<span>История</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=home')}>
|
||
<span className={cl.navIcon}><Icon name="home" size={15} /></span>
|
||
<span>Главная</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=my')}>
|
||
<span className={cl.navIcon}><Icon name="folder" size={15} /></span>
|
||
<span>Мои игры</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=templates')}>
|
||
<span className={cl.navIcon}><Icon name="grid" size={15} /></span>
|
||
<span>Шаблоны</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=archive')}>
|
||
<span className={cl.navIcon}><Icon name="archive" size={15} /></span>
|
||
<span>Архив</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/docs')}>
|
||
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
|
||
<span>ВИКИ</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<div className={cl.sidebarFooter}>
|
||
<button
|
||
className={cl.docsBtn}
|
||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||
onClick={() => navigate('/rules')}
|
||
>
|
||
<Icon name="shield" size={15} /> Правила создания игр
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* === Основной контент === */}
|
||
<main className={cl.main}>
|
||
<header className={cl.topBar}>
|
||
<div>
|
||
<h1 className={cl.pageTitle}>
|
||
<Icon name="image" size={13} /> Кит hero-блоков
|
||
</h1>
|
||
<p className={cl.pageSub}>
|
||
10 вариантов главного баннера Studio — выбери тот, что нравится
|
||
</p>
|
||
</div>
|
||
<div className={cl.topBarActions}>
|
||
<button className={cl.searchBox} onClick={() => navigate('/')}>
|
||
← В Studio
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="hkIntro">
|
||
Каждый блок ниже — реальный кадр игры Рублокса. Это варианты
|
||
того, как будет выглядеть главный баннер на странице Studio
|
||
(вместо нынешнего с кубиками). Посмотри и скажи мне номер —
|
||
вставлю выбранный на главную.
|
||
</div>
|
||
|
||
{/* 10 вариантов */}
|
||
{VARIANTS.map((v) => (
|
||
<section
|
||
key={v.id}
|
||
className={'hkCard' + (chosen === v.id ? ' hkCard--chosen' : '')}
|
||
>
|
||
<div className="hkCardHead">
|
||
<span className="hkNum">Вариант {v.id}</span>
|
||
<button
|
||
className={'hkPick' + (chosen === v.id ? ' hkPick--on' : '')}
|
||
onClick={() => setChosen(chosen === v.id ? null : v.id)}
|
||
>
|
||
{chosen === v.id
|
||
? <><Icon name="check" size={13} /> Этот нравится</>
|
||
: 'Мне нравится этот'}
|
||
</button>
|
||
</div>
|
||
<HeroPreview v={v} />
|
||
</section>
|
||
))}
|
||
|
||
{/* Подсказка о выборе */}
|
||
<div className="hkFooter">
|
||
{chosen ? (
|
||
<>
|
||
<div className="hkFooterIco"><Icon name="check" size={22} /></div>
|
||
<div>
|
||
<b>Отмечен вариант {chosen}.</b> Напиши мне в чат
|
||
«ставь вариант {chosen}» — и я вставлю его на главную
|
||
страницу Studio.
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="hkFooterIco hkFooterIco--idle">
|
||
<Icon name="image" size={22} />
|
||
</div>
|
||
<div>
|
||
Отметь понравившийся вариант кнопкой «Мне нравится
|
||
этот» или просто напиши мне его номер в чат.
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// Инлайн-стили
|
||
// ══════════════════════════════════════════════════════════════════
|
||
const INLINE_STYLES = `
|
||
.hkIntro {
|
||
background: #eef2ff;
|
||
border: 1px solid #c7d2fe;
|
||
border-radius: 14px;
|
||
padding: 14px 18px;
|
||
margin-bottom: 22px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #3730a3;
|
||
}
|
||
|
||
/* === Карточка варианта === */
|
||
.hkCard {
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 20px;
|
||
padding: 14px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
|
||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||
}
|
||
.hkCard--chosen {
|
||
border-color: #16a34a;
|
||
box-shadow: 0 8px 28px rgba(22, 163, 74, 0.18);
|
||
}
|
||
.hkCardHead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 4px 6px 12px;
|
||
}
|
||
.hkNum {
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
color: #0f172a;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
.hkPick {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: #f1f5f9;
|
||
border: 1px solid #e2e8f0;
|
||
color: #475569;
|
||
font-size: 12.5px;
|
||
font-weight: 800;
|
||
padding: 7px 14px;
|
||
border-radius: 50px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all 160ms ease;
|
||
}
|
||
.hkPick:hover { background: #e2e8f0; }
|
||
.hkPick--on, .hkPick--on:hover {
|
||
background: #16a34a;
|
||
border-color: #16a34a;
|
||
color: #fff;
|
||
}
|
||
|
||
/* === Hero-блок (то, что встанет на главную) === */
|
||
.hkHero {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 1920 / 640;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
background: #1e293b;
|
||
}
|
||
.hkHeroImg {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* оверлеи для читаемости текста */
|
||
.hkOverlay { position: absolute; inset: 0; pointer-events: none; }
|
||
.hkOverlay--left-dark {
|
||
background: linear-gradient(100deg,
|
||
rgba(8, 11, 22, 0.86) 0%,
|
||
rgba(8, 11, 22, 0.62) 38%,
|
||
rgba(8, 11, 22, 0.06) 66%,
|
||
transparent 100%);
|
||
}
|
||
.hkOverlay--bottom-dark {
|
||
background: linear-gradient(to top,
|
||
rgba(8, 11, 22, 0.90) 0%,
|
||
rgba(8, 11, 22, 0.45) 36%,
|
||
transparent 70%);
|
||
}
|
||
.hkOverlay--full-soft {
|
||
background: radial-gradient(ellipse at center,
|
||
rgba(8, 11, 22, 0.30) 0%,
|
||
rgba(8, 11, 22, 0.66) 100%);
|
||
}
|
||
.hkOverlay--panel {
|
||
background: linear-gradient(90deg,
|
||
rgba(8, 11, 22, 0.20) 0%, transparent 55%);
|
||
}
|
||
|
||
/* контейнер текста */
|
||
.hkHeroInner {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 56px;
|
||
z-index: 2;
|
||
}
|
||
.hkHeroInner--left { justify-content: flex-start; }
|
||
.hkHeroInner--center { justify-content: center; text-align: center; }
|
||
.hkHeroInner--panel { justify-content: flex-start; }
|
||
|
||
.hkHeroBox { max-width: 560px; }
|
||
.hkHeroInner--center .hkHeroBox { max-width: 680px; }
|
||
/* вариант 'panel' — текст в полупрозрачной карточке */
|
||
.hkHeroInner--panel .hkHeroBox {
|
||
max-width: 480px;
|
||
background: rgba(12, 16, 28, 0.74);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 18px;
|
||
padding: 28px 30px;
|
||
}
|
||
|
||
.hkBadge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
border: 1px solid rgba(255, 255, 255, 0.34);
|
||
color: #fff;
|
||
padding: 4px 13px;
|
||
border-radius: 999px;
|
||
font-size: 10.5px;
|
||
font-weight: 800;
|
||
letter-spacing: 1.5px;
|
||
text-transform: uppercase;
|
||
margin-bottom: 14px;
|
||
}
|
||
.hkHeroInner--center .hkBadge { margin-left: auto; margin-right: auto; }
|
||
|
||
.hkTitle {
|
||
margin: 0 0 12px;
|
||
font-size: 38px;
|
||
line-height: 1.08;
|
||
font-weight: 900;
|
||
color: #fff;
|
||
letter-spacing: -1px;
|
||
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
|
||
}
|
||
.hkDesc {
|
||
margin: 0 0 22px;
|
||
font-size: 15.5px;
|
||
line-height: 1.55;
|
||
color: rgba(255, 255, 255, 0.92);
|
||
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
|
||
}
|
||
.hkBtn {
|
||
display: inline-block;
|
||
padding: 13px 26px;
|
||
background: #fff;
|
||
color: #3357ff;
|
||
border-radius: 12px;
|
||
font-size: 14.5px;
|
||
font-weight: 900;
|
||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.3);
|
||
letter-spacing: 0.2px;
|
||
}
|
||
|
||
/* === Подвал === */
|
||
.hkFooter {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
border-radius: 16px;
|
||
padding: 18px 22px;
|
||
margin-top: 8px;
|
||
font-size: 14px;
|
||
color: #14532d;
|
||
line-height: 1.55;
|
||
}
|
||
.hkFooterIco {
|
||
flex-shrink: 0;
|
||
width: 44px; height: 44px;
|
||
border-radius: 12px;
|
||
background: #16a34a;
|
||
color: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.hkFooterIco--idle { background: #94a3b8; }
|
||
|
||
/* Адаптив — на узких экранах текст крупного hero уменьшаем */
|
||
@media (max-width: 1100px) {
|
||
.hkHeroInner { padding: 0 32px; }
|
||
.hkTitle { font-size: 30px; }
|
||
.hkDesc { font-size: 14px; }
|
||
}
|
||
`;
|
||
|
||
export default KubikonHeroKit;
|