diff --git a/src/editor/HierarchyPanel.module.css b/src/editor/HierarchyPanel.module.css index f1f5404..509e62a 100644 --- a/src/editor/HierarchyPanel.module.css +++ b/src/editor/HierarchyPanel.module.css @@ -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); diff --git a/src/editor/Icon.jsx b/src/editor/Icon.jsx index 17dd379..3f24845 100644 --- a/src/editor/Icon.jsx +++ b/src/editor/Icon.jsx @@ -73,6 +73,9 @@ const GLYPHS = { 'arrow-down': () => (<>), 'arrow-left': () => (<>), 'arrow-right': () => (<>), + // Полноэкранный режим: 4 уголка наружу / внутрь. + 'fullscreen': () => (<>), + 'fullscreen-exit': () => (<>), refresh: () => (<>), cycle: () => (<>), flag: () => (<>), diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx index f234316..8f98726 100644 --- a/src/editor/KubikonEditor.jsx +++ b/src/editor/KubikonEditor.jsx @@ -47,6 +47,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 секунд тишины → авто-сохранение // Шаблон глобального скрипта. @@ -470,6 +475,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); @@ -2146,7 +2243,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 @@ -2309,6 +2408,18 @@ const KubikonEditor = () => { )} + {/* Полноэкранный режим — съедает верхнюю панель браузера. + В десктоп-приложении не нужен (окно и так на весь экран). */} + {!IS_DESKTOP_APP && ( + + )} {/* Кнопка баг-репорта в шапке — кликает по скрытой плавающей. */}