feat(studio): компактный UI + fullscreen + desktop-guard
All checks were successful
CI / Lint (pull_request) Successful in 1m10s
CI / Build (pull_request) Successful in 2m7s
CI / Secret scan (pull_request) Successful in 24s
CI / PR size check (pull_request) Successful in 7s
CI / Deploy to S1 + S2 (pull_request) Has been skipped

UI (ближе к Roblox Studio):
- интерфейс уменьшен ~10% (шрифты/паддинги/topbar 56→50);
- правая панель шире (280→320), компактные строки в «Объектах сцены»;
- перетаскиваемая граница между списком объектов и свойствами (доля в
  localStorage).

Fullscreen редактора:
- кнопка «На весь экран» в шапке + автовход при первом клике (браузер
  требует user gesture);
- иконки fullscreen/fullscreen-exit.

Desktop-guard (window.__RUBLOX_DESKTOP__):
- в Electron-приложении авто-fullscreen отключён (окно и так на весь
  экран): не дёргаем FS при автовходе, при «Запустить», кнопка FS скрыта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
min 2026-06-15 20:17:58 +03:00
parent ade30363c8
commit a9a9668071
5 changed files with 195 additions and 21 deletions

View File

@ -1,8 +1,10 @@
/* === Hierarchy Panel === */
/* Компактные строки (как Roblox Explorer): меньше вертикальных отступов
больше объектов влезает без скролла. */
.hierarchy {
flex: 1;
overflow-y: auto;
padding: 6px 0;
padding: 4px 0;
font-size: 12px;
user-select: none;
position: relative;
@ -13,13 +15,13 @@
}
.rootLine {
padding: 6px 8px;
padding: 3px 8px;
color: var(--text);
font-size: 13px;
font-size: 12px;
}
.systemItem {
padding: 4px 8px 4px 28px;
padding: 2px 8px 2px 26px;
color: var(--text-dim);
font-size: 12px;
}
@ -28,11 +30,11 @@
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
padding: 3px 8px;
color: var(--text);
cursor: pointer;
border-radius: 4px;
margin-top: 6px;
margin-top: 3px;
font-weight: 600;
}
@ -72,8 +74,8 @@
.item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
gap: 7px;
padding: 2px 8px;
cursor: pointer;
border-radius: 4px;
color: var(--text);

View File

@ -73,6 +73,9 @@ const GLYPHS = {
'arrow-down': () => (<><path d="M12 5v14" {...S}/><path d="M6 13l6 6 6-6" {...S}/></>),
'arrow-left': () => (<><path d="M19 12H5" {...S}/><path d="M11 6l-6 6 6 6" {...S}/></>),
'arrow-right': () => (<><path d="M5 12h14" {...S}/><path d="M13 6l6 6-6 6" {...S}/></>),
// Полноэкранный режим: 4 уголка наружу / внутрь.
'fullscreen': () => (<><path d="M4 9V4h5" {...S}/><path d="M20 9V4h-5" {...S}/><path d="M4 15v5h5" {...S}/><path d="M20 15v5h-5" {...S}/></>),
'fullscreen-exit': () => (<><path d="M9 4v5H4" {...S}/><path d="M15 4v5h5" {...S}/><path d="M9 20v-5H4" {...S}/><path d="M15 20v-5h5" {...S}/></>),
refresh: () => (<><path d="M20 11a8 8 0 0 0-14-4.5L4 9" {...S}/><path d="M4 4v5h5" {...S}/><path d="M4 13a8 8 0 0 0 14 4.5L20 15" {...S}/><path d="M20 20v-5h-5" {...S}/></>),
cycle: () => (<><path d="M20 11a8 8 0 0 0-14-4.5L4 9" {...S}/><path d="M4 4v5h5" {...S}/><path d="M4 13a8 8 0 0 0 14 4.5L20 15" {...S}/><path d="M20 20v-5h-5" {...S}/></>),
flag: () => (<><path d="M6 21V4" {...S}/><path d="M6 4h11l-2.5 4L17 12H6" {...S}/></>),

View File

@ -46,6 +46,11 @@ import cl from './KubikonEditor.module.css';
import Icon from './Icon';
import ConfirmModal from './ConfirmModal';
// В десктоп-приложении (Electron-обёртка, см. rublox-desktop) окно и так на
// весь экран без браузерной панели fullscreen НЕ нужен. preload выставляет
// window.__RUBLOX_DESKTOP__. Глушим авто-fullscreen, чтобы не дёргать окно.
const IS_DESKTOP_APP = typeof window !== 'undefined' && !!window.__RUBLOX_DESKTOP__;
const AUTOSAVE_DEBOUNCE_MS = 10000; // 10 секунд тишины авто-сохранение
// Шаблон глобального скрипта.
@ -469,6 +474,98 @@ const KubikonEditor = () => {
const canvasRef = useRef(null);
const sceneRef = useRef(null);
// === Fullscreen редактора ===
// Верхняя панель браузера съедает ~20% экрана. Браузер НЕ даёт включить
// fullscreen автоматически при загрузке (нужен user gesture), поэтому:
// 1) кнопка в шапке;
// 2) автоматический вход при ПЕРВОМ клике пользователя по редактору.
const [isFullscreen, setIsFullscreen] = useState(false);
const fsAutoTriedRef = useRef(false); // авто-вход пробуем только 1 раз
const requestEditorFullscreen = React.useCallback(() => {
try {
const root = document.documentElement;
const req = root.requestFullscreen
|| root.webkitRequestFullscreen
|| root.mozRequestFullScreen
|| root.msRequestFullscreen;
if (req && !document.fullscreenElement) req.call(root).catch(() => {});
} catch (e) { /* юзер запретил — работаем в окне */ }
}, []);
const exitEditorFullscreen = React.useCallback(() => {
try {
const ex = document.exitFullscreen || document.webkitExitFullscreen
|| document.mozCancelFullScreen || document.msExitFullscreen;
if (ex && document.fullscreenElement) ex.call(document).catch?.(() => {});
} catch (e) { /* ignore */ }
}, []);
const toggleEditorFullscreen = React.useCallback(() => {
if (document.fullscreenElement) exitEditorFullscreen();
else requestEditorFullscreen();
}, [requestEditorFullscreen, exitEditorFullscreen]);
// Следим за состоянием fullscreen (кнопка показывает актуальную иконку).
useEffect(() => {
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', onFsChange);
document.addEventListener('webkitfullscreenchange', onFsChange);
return () => {
document.removeEventListener('fullscreenchange', onFsChange);
document.removeEventListener('webkitfullscreenchange', onFsChange);
};
}, []);
// Автовход в fullscreen при ПЕРВОМ клике/нажатии по редактору. Один раз.
useEffect(() => {
// В десктоп-приложении окно и так на весь экран авто-FS не нужен.
if (IS_DESKTOP_APP) return;
const tryAuto = () => {
if (fsAutoTriedRef.current) return;
fsAutoTriedRef.current = true;
if (!document.fullscreenElement) requestEditorFullscreen();
};
window.addEventListener('pointerdown', tryAuto, { once: true, capture: true });
window.addEventListener('keydown', tryAuto, { once: true, capture: true });
return () => {
window.removeEventListener('pointerdown', tryAuto, { capture: true });
window.removeEventListener('keydown', tryAuto, { capture: true });
};
}, [requestEditorFullscreen]);
// При уходе со страницы редактора выходим из fullscreen.
useEffect(() => () => { exitEditorFullscreen(); }, [exitEditorFullscreen]);
// === Регулируемая граница между «Объекты сцены» и «Свойства» ===
// Доля высоты под список объектов (0.2..0.85). Сохраняем в localStorage.
const [hierFraction, setHierFraction] = useState(() => {
const v = parseFloat(localStorage.getItem('rbxStudioHierFraction'));
return Number.isFinite(v) && v >= 0.2 && v <= 0.85 ? v : 0.5;
});
const rightPanelRef = useRef(null);
const splitDragRef = useRef(false);
useEffect(() => {
const onMove = (e) => {
if (!splitDragRef.current || !rightPanelRef.current) return;
const rect = rightPanelRef.current.getBoundingClientRect();
// Доля от верха панели до курсора (за вычетом верхнего заголовка ~24px).
let f = (e.clientY - rect.top - 24) / Math.max(1, rect.height - 24);
f = Math.max(0.2, Math.min(0.85, f));
setHierFraction(f);
};
const onUp = () => {
if (!splitDragRef.current) return;
splitDragRef.current = false;
document.body.style.cursor = '';
try { localStorage.setItem('rbxStudioHierFraction', String(hierFraction)); } catch (_) {}
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
return () => {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
};
}, [hierFraction]);
const startSplitDrag = React.useCallback((e) => {
e.preventDefault();
splitDragRef.current = true;
document.body.style.cursor = 'row-resize';
}, []);
// Team Create клиент совместного редактирования + presence-overlay.
const collabRef = useRef(null);
const collabOverlayRef = useRef(null);
@ -2136,7 +2233,9 @@ const KubikonEditor = () => {
// fullscreen иначе Ctrl+W/Ctrl+D случайно закрывают вкладку
// в режиме игры. Это user gesture (клик по кнопке Play),
// поэтому requestFullscreen() разрешён.
try {
// В десктоп-приложении этого риска нет (нет вкладок браузера)
// окно и так на весь экран, FS не нужен.
if (!IS_DESKTOP_APP) try {
const root = document.documentElement;
const req = root.requestFullscreen
|| root.webkitRequestFullscreen
@ -2299,6 +2398,18 @@ const KubikonEditor = () => {
</button>
</>
)}
{/* Полноэкранный режим съедает верхнюю панель браузера.
В десктоп-приложении не нужен (окно и так на весь экран). */}
{!IS_DESKTOP_APP && (
<button
onClick={toggleEditorFullscreen}
title={isFullscreen ? 'Выйти из полноэкранного режима (F11)' : 'На весь экран (F11)'}
className={cl.toolbarBtn}
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 38, height: 38, padding: 0, flexShrink: 0 }}
>
<Icon name={isFullscreen ? 'fullscreen-exit' : 'fullscreen'} size={16} />
</button>
)}
{/* Кнопка баг-репорта в шапке — кликает по скрытой плавающей. */}
<button
onClick={() => document.querySelector('[data-kubikon-bug-btn]')?.click()}
@ -3510,7 +3621,8 @@ const KubikonEditor = () => {
</div>
{/* Правая панель — Hierarchy + Inspector */}
<aside className={cl.rightPanel}>
<aside className={cl.rightPanel} ref={rightPanelRef}>
<div className={cl.rightSection} style={{ flexGrow: hierFraction, flexBasis: 0 }}>
<div className={cl.panelTitle}>Объекты сцены</div>
<HierarchyPanel
blocks={blocksList}
@ -3735,7 +3847,16 @@ const KubikonEditor = () => {
onAssignToFolder={(kind, ref, folderId) =>
sceneRef.current?.assignToFolder(kind, ref, folderId)}
/>
</div>
{/* Перетаскиваемая граница между списком объектов и свойствами. */}
<div
className={cl.rightSplitter}
onPointerDown={startSplitDrag}
title="Потяните, чтобы изменить высоту"
/>
<div className={cl.rightSection} style={{ flexGrow: (1 - hierFraction), flexBasis: 0 }}>
<div className={cl.panelTitle}>Свойства</div>
<InspectorPanel
selection={selection}
@ -3872,6 +3993,7 @@ const KubikonEditor = () => {
markDirty();
}}
/>
</div>
</aside>
</div>

View File

@ -31,7 +31,8 @@
font-family: "Roboto Condensed", system-ui, -apple-system, sans-serif;
display: grid;
grid-template-rows: 56px auto 1fr 28px;
/* UI уменьшен на ~10% (ближе к Roblox Studio): topbar 56→50, status 28→26. */
grid-template-rows: 50px auto 1fr 26px;
height: 100vh;
width: 100%;
max-width: 100%;
@ -39,7 +40,7 @@
color: var(--text);
overflow: hidden;
box-sizing: border-box;
font-size: 14px;
font-size: 13px;
}
.editor *,
@ -52,8 +53,8 @@
.topBar {
display: flex;
align-items: center;
gap: 16px;
padding: 0 16px;
gap: 12px;
padding: 0 14px;
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-darkest) 100%);
border-bottom: 2px solid var(--border);
z-index: 10;
@ -145,12 +146,12 @@
}
.toolbarBtn {
padding: 8px 16px;
padding: 7px 13px;
background: var(--bg-mid);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 13px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
font-weight: 600;
@ -210,7 +211,9 @@
/* === WORKSPACE === */
.workspace {
display: grid;
grid-template-columns: 240px minmax(0, 1fr) 280px;
/* Правая панель шире (280320) длинные имена объектов не обрезаются,
больше места под список и свойства. Левая чуть уже (240224). */
grid-template-columns: 224px minmax(0, 1fr) 320px;
overflow: hidden;
min-height: 0;
}
@ -233,18 +236,61 @@
.rightPanel {
border-left: 2px solid var(--border);
overflow: hidden; /* секции скроллятся внутри себя, панель — нет */
}
/* Секция правой панели (список объектов / свойства) занимает свою долю
высоты, скроллится независимо. Доля регулируется сплиттером. */
.rightSection {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* Перетаскиваемая граница между списком объектов и свойствами. */
.rightSplitter {
height: 6px;
flex-shrink: 0;
cursor: row-resize;
background: var(--bg-mid);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
position: relative;
transition: background 0.12s;
}
.rightSplitter::before {
/* визуальная «ручка» — три точки по центру */
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 28px;
height: 2px;
border-radius: 2px;
background: var(--text-dim);
opacity: 0.5;
}
.rightSplitter:hover {
background: var(--accent);
}
.rightSplitter:hover::before {
background: #fff;
opacity: 0.9;
}
.panelTitle {
padding: 12px 16px;
font-size: 11px;
padding: 7px 14px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
letter-spacing: 1.3px;
color: var(--text-dim);
background: var(--bg-mid);
border-bottom: 1px solid var(--border);
border-top: 1px solid var(--border);
font-weight: 800;
flex-shrink: 0;
}
.panelTitle:first-child {

View File

@ -789,7 +789,8 @@ const KubikonPlayer = () => {
|| root.webkitRequestFullscreen
|| root.mozRequestFullScreen
|| root.msRequestFullscreen;
if (req) {
// В десктоп-приложении (Electron) окно и так на весь экран FS не нужен.
if (req && !(typeof window !== 'undefined' && window.__RUBLOX_DESKTOP__)) {
try { await req.call(root); } catch (e) { /* отменено */ }
}
setMobileStartTapped(true);