studio/src/community/KubikonHeroKit.jsx
МИН 61fba4e174
Some checks failed
CI / Lint + Format (push) Failing after 32s
CI / Build (push) Failing after 37s
CI / Secret scan (push) Failing after 37s
CI / PR size check (push) Has been skipped
fix: починка билда + studio.rublox.pro инфра
Большой консолидирующий коммит после поднятия 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>
2026-05-28 05:01:13 +03:00

469 lines
18 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, { 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;