From 5e1a0edf9b8b55b9217ad3e86d48c8ec6e546841 Mon Sep 17 00:00:00 2001 From: min Date: Fri, 5 Jun 2026 01:33:39 +0300 Subject: [PATCH] =?UTF-8?q?feat(studio):=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=2017=20=E2=80=94=20Toolbox=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=20Roblox=20Creator=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Единый Toolbox вместо отдельной кнопки «Модель» в панели «Создать»: - 4 верхние вкладки как в Roblox: Магазин / Инвентарь / Недавние / Советы. - Магазин: главный экран с 6 плитками-категориями (3D-объекты / Эффекты / 2D-картинки / Готовые механики / Плагины / Аудио) + ряд «Популярное» (FREE). - Клик по категории → детальный список с поиском и подкатегориями; «← Категории». - 3D-объекты = 700+ моделей; Эффекты = эмиттер/луч/указатель/свет/триггер; Готовые механики = 12 китов; 2D/Плагины/Аудио = «Скоро будет». - Инвентарь = мои воксельные модели; Недавние = модели сообщества; Советы = гайд. - TopRibbon: кнопка «Модель» → «Toolbox» (открывает магазин); вкладка «Модель» переименована в «Редактор моделей» (создание своих воксельных ассетов). - CSS: topTabs/catGrid/catTile/trendRow/breadcrumb/soon/tips/freeBadge. Вся прежняя логика моделей (lazy-load, лайки, thumbnails) сохранена внутри новой структуры. Esc в категории → назад к плиткам. Co-Authored-By: Claude Opus 4.8 --- src/editor/ToolboxModal.jsx | 397 +++++++++++++++++------------ src/editor/ToolboxModal.module.css | 131 ++++++++++ src/editor/TopRibbon.jsx | 16 +- 3 files changed, 375 insertions(+), 169 deletions(-) diff --git a/src/editor/ToolboxModal.jsx b/src/editor/ToolboxModal.jsx index b1e655f..c5b78de 100644 --- a/src/editor/ToolboxModal.jsx +++ b/src/editor/ToolboxModal.jsx @@ -282,6 +282,13 @@ const ToolboxModal = ({ initialSection = 'standard', }) => { // Корневой раздел: 'standard' | 'mine' | 'community' + // === Roblox-style Toolbox (задача 17) === + // Верхняя вкладка: 'store' | 'inventory' | 'recent' | 'tips'. + const [view, setView] = useState('store'); + // Выбранная категория магазина (null = главный экран с 6 плитками): + // '3d' | 'fx' | '2d' | 'gameplay' | 'plugins' | 'audio'. + const [storeCat, setStoreCat] = useState(null); + const [section, setSection] = useState('standard'); const [search, setSearch] = useState(''); const [category, setCategory] = useState('all'); // для 'standard' @@ -297,6 +304,9 @@ const ToolboxModal = ({ useEffect(() => { if (open) { setSearch(''); + // initialSection маппится в новую структуру: mine → inventory. + if (initialSection === 'mine') { setView('inventory'); setStoreCat(null); } + else { setView('store'); setStoreCat(null); } setSection(initialSection || 'standard'); setCategory('all'); setUserKind('all'); @@ -307,13 +317,30 @@ const ToolboxModal = ({ } }, [open, initialSection]); - // Esc — закрыть + // Маппинг категории магазина → внутренний section (для lazy-load моделей). + const STORE_CAT_TO_SECTION = { '3d': 'standard', gameplay: 'gameplay', '2d': 'standard' }; + const openStoreCategory = useCallback((catId) => { + setStoreCat(catId); + setSearch(''); + setCategory('all'); + setKitCat('all'); + const sec = STORE_CAT_TO_SECTION[catId]; + if (sec) setSection(sec); + }, []); + + // Синхронизация верхней вкладки → внутренний section (для lazy-load). + useEffect(() => { + if (view === 'inventory') setSection('mine'); + else if (view === 'recent') setSection('community'); + }, [view]); + + // Esc — закрыть (если открыта категория магазина — сначала назад к плиткам) useEffect(() => { if (!open) return; - const onKey = (e) => { if (e.key === 'Escape') onClose(); }; + const onKey = (e) => { if (e.key === 'Escape') { if (view === 'store' && storeCat) setStoreCat(null); else onClose(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); - }, [open, onClose]); + }, [open, onClose, view, storeCat]); // Lazy-load моих моделей при переключении на 'mine' useEffect(() => { @@ -438,6 +465,27 @@ const ToolboxModal = ({ ? (myModels?.length || 0) : (communityModels?.length || 0); + // 6 категорий магазина (как в Roblox Creator Store). + const STORE_CATEGORIES = [ + { id: '3d', label: '3D-объекты', icon: 'cube', desc: '700+ моделей: природа, дома, мебель, NPC' }, + { id: 'fx', label: 'Эффекты', icon: 'sparkles', desc: 'Частицы, лучи, маркеры' }, + { id: '2d', label: '2D-картинки', icon: 'image', desc: 'Иконки и текстуры для интерфейса' }, + { id: 'gameplay', label: 'Готовые механики', icon: 'zap', desc: '12 механик: вставил — работает' }, + { id: 'plugins', label: 'Плагины', icon: 'puzzle', desc: 'Расширения студии' }, + { id: 'audio', label: 'Аудио', icon: 'sound', desc: 'Звуки и музыка' }, + ]; + // Trending — что популярно (для главного экрана магазина). Берём яркие киты. + const TRENDING = GAMEPLAY_KITS.filter(k => + ['shift-to-run', 'day-night-cycle', 'loot-crate', 'confetti'].includes(k.id)); + // Эффекты-примитивы для категории «Эффекты». + const FX_ITEMS = [ + { id: 'emitter', name: 'Эмиттер частиц', icon: 'sparkles', desc: 'Источник частиц (огонь/искры/дым)' }, + { id: 'beam', name: 'Луч (beam)', icon: 'zap', desc: 'Бегущий луч между точками' }, + { id: 'pointer', name: 'Указатель-стрелка', icon: 'arrow-up', desc: 'Парящая стрелка-подсказка' }, + { id: 'light', name: 'Источник света', icon: 'sun', desc: 'Точечная лампа' }, + { id: 'checkpoint', name: 'Триггер-зона', icon: 'flag', desc: 'Невидимая зона-триггер' }, + ]; + if (!open) return null; // Обработчик выбора пользовательской модели — пока stub. @@ -503,67 +551,63 @@ const ToolboxModal = ({ return (
{ if (e.target === e.currentTarget) onClose(); }}>
+ {/* === Шапка с 4 верхними вкладками (как Roblox Creator Store) === */}

- Тулбокс — библиотека объектов + Toolbox

-
- {section === 'standard' - ? `Показано ${visibleCount} из ${totalForSection}` - : section === 'gameplay' - ? `${kitsFiltered.length} готовых механик` - : (myModels === null && section === 'mine') || (communityModels === null && section === 'community') - ? '...' - : `${visibleCount} из ${totalForSection}`} -
- {/* Раздел: Стандартные / Мои / Сообщество */} -
- - - - +
+ {[ + { id: 'store', label: 'Магазин', icon: 'box' }, + { id: 'inventory', label: 'Инвентарь', icon: 'grid' }, + { id: 'recent', label: 'Недавние', icon: 'clock' }, + { id: 'tips', label: 'Советы', icon: 'bulb' }, + ].map(t => ( + + ))}
-
- setSearch(e.target.value)} - autoFocus - /> -
+ {/* Поиск — скрыт только на главном экране магазина и в советах */} + {!(view === 'store' && !storeCat) && view !== 'tips' && ( +
+ setSearch(e.target.value)} + autoFocus + /> +
+ )} - {/* Подкатегории зависят от раздела */} - {section === 'standard' && ( + {/* Хлебные крошки/назад при открытой категории магазина */} + {view === 'store' && storeCat && ( +
+ + + {(STORE_CATEGORIES.find(c => c.id === storeCat) || {}).label} + +
+ )} + + {/* Подкатегории standard (3D) */} + {view === 'store' && storeCat === '3d' && (
{standardCategoriesWithCount.map(c => ( + ))} +
+ )} + {/* Фильтр kind для инвентаря */} + {view === 'inventory' && ( +
+ + + +
+ )} + + {/* ====================== КОНТЕНТ ====================== */} + + {/* --- МАГАЗИН: главный экран (6 плиток + Trending) --- */} + {view === 'store' && !storeCat && ( +
+
Категории
+
+ {STORE_CATEGORIES.map(c => ( + + ))} +
+
+ Популярное +
+
+ {TRENDING.map(kit => ( + + ))} +
+
+ )} + + {/* --- МАГАЗИН: категория 3D-объекты --- */} + {view === 'store' && storeCat === '3d' && ( +
+ {standardFiltered.length === 0 + ?
Ничего не найдено
+ : standardFiltered.map(m => ( + { onPick(m.id); onClose(); }} /> + ))} +
+ )} + + {/* --- МАГАЗИН: Эффекты --- */} + {view === 'store' && storeCat === 'fx' && ( +
+ {FX_ITEMS.filter(f => !search.trim() || f.name.toLowerCase().includes(search.trim().toLowerCase())).map(f => ( + ))}
)} - {(section === 'mine' || section === 'community') && ( -
- - - + {/* --- МАГАЗИН: Готовые механики --- */} + {view === 'store' && storeCat === 'gameplay' && ( +
+ {kitsFiltered.length === 0 + ?
Ничего не найдено
+ : kitsFiltered.map(kit => ( + + ))}
)} - {/* === Контент === */} -
- {section === 'standard' && ( - standardFiltered.length === 0 ? ( -
Ничего не найдено
- ) : ( - standardFiltered.map(m => ( - { onPick(m.id); onClose(); }} - /> - )) - ) - )} + {/* --- МАГАЗИН: 2D-картинки / Плагины / Аудио — пока «скоро» --- */} + {view === 'store' && (storeCat === '2d' || storeCat === 'plugins' || storeCat === 'audio') && ( +
+ +
Скоро будет
+
+ {storeCat === '2d' && 'Иконки и текстуры для интерфейса появятся в следующем обновлении.'} + {storeCat === 'plugins' && 'Плагины-расширения студии — в разработке (фаза T4).'} + {storeCat === 'audio' && 'Библиотека звуков и музыки — в разработке.'} +
+
+ )} - {section === 'mine' && ( - loading || myModels === null ? ( + {/* --- ИНВЕНТАРЬ: мои модели --- */} + {view === 'inventory' && ( +
+ {loading || myModels === null ? (
⏳ Загрузка...
) : !userId ? ( -
- Войдите в аккаунт, чтобы видеть свои модели -
+
Войдите в аккаунт, чтобы видеть свои модели
) : loadError ? (
{loadError}
) : mineFiltered.length === 0 ? (
{myModels.length === 0 - ? 'У вас пока нет своих моделей. Создайте их во вкладке «Модель» → «Воксельная» или «Гладкая».' + ? 'У вас пока нет своих моделей. Создайте их в воксельном редакторе.' : 'Ничего не найдено по фильтру'}
) : ( mineFiltered.map(m => ( - handlePickUserModel(m)} - isMine - onEdit={onEditUserModel} - onSettings={onUserModelSettings} - onDelete={onDeleteUserModel} - /> + handlePickUserModel(m)} isMine + onEdit={onEditUserModel} onSettings={onUserModelSettings} onDelete={onDeleteUserModel} /> )) - ) - )} + )} +
+ )} - {section === 'community' && ( - loading || communityModels === null ? ( + {/* --- НЕДАВНИЕ: сообщество (популярные модели сообщества) --- */} + {view === 'recent' && ( +
+ {communityModels === null ? (
⏳ Загрузка...
- ) : loadError ? ( -
{loadError}
) : communityFiltered.length === 0 ? ( -
- {communityModels.length === 0 - ? 'Пока нет опубликованных моделей сообщества. Будь первым!' - : 'Ничего не найдено по фильтру'} -
+
Пока пусто. Используй ассеты — они появятся здесь.
) : ( communityFiltered.map(m => ( - handlePickUserModel(m)} isMine={userId != null && m.user_id === userId} - onEdit={onEditUserModel} - onSettings={onUserModelSettings} - onDelete={onDeleteUserModel} - showSocial - onLike={handleLikeModel} - /> + onEdit={onEditUserModel} onSettings={onUserModelSettings} onDelete={onDeleteUserModel} + showSocial onLike={handleLikeModel} /> )) - ) - )} + )} +
+ )} - {section === 'gameplay' && ( - kitsFiltered.length === 0 ? ( -
Ничего не найдено
- ) : ( - kitsFiltered.map(kit => ( - - )) - ) - )} -
+ {/* --- СОВЕТЫ --- */} + {view === 'tips' && ( +
+

Как пользоваться Toolbox

+
    +
  • 3D-объекты — 700+ готовых моделей: деревья, дома, мебель, персонажи. Клик → объект появляется на сцене.
  • +
  • Готовые механики — вставь поведение одним кликом: бег на Shift, смена дня/ночи, сундук с лутом, счётчик монет. Скрипт прикрепляется сам.
  • +
  • Эффекты — частицы, лучи, источники света, триггер-зоны.
  • +
  • Инвентарь — твои воксельные модели, созданные в редакторе.
  • +
  • Жми на категорию, ищи через поиск, кликни ассет — он добавится в проект.
  • +
+

Собери целую игру, не написав ни строчки кода — просто перетаскивая готовые механики.

+
+ )}
); diff --git a/src/editor/ToolboxModal.module.css b/src/editor/ToolboxModal.module.css index df93cf3..40b2c5f 100644 --- a/src/editor/ToolboxModal.module.css +++ b/src/editor/ToolboxModal.module.css @@ -447,3 +447,134 @@ font-size: 36px; color: var(--text-dim); } + +/* ====================== Roblox-style Toolbox (задача 17) ====================== */ +.topTabs { + display: flex; + gap: 2px; + padding: 0 14px; + border-bottom: 1px solid var(--border, rgba(255,255,255,0.08)); + flex: 0 0 auto; +} +.topTab { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + padding: 10px 4px 8px; + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-dim, #9aa3b2); + font-size: 12px; + cursor: pointer; + transition: color .12s, border-color .12s; +} +.topTab:hover { color: var(--text, #e8ecf2); } +.topTabActive { + color: var(--accent, #4d6bff); + border-bottom-color: var(--accent, #4d6bff); +} + +.breadcrumb { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px 4px; + flex: 0 0 auto; +} +.backBtn { + display: inline-flex; + align-items: center; + gap: 4px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.1); + color: var(--text, #e8ecf2); + padding: 5px 10px; + border-radius: 8px; + font-size: 13px; + cursor: pointer; +} +.backBtn:hover { background: rgba(255,255,255,0.12); } +.crumbCurrent { font-weight: 700; color: var(--text, #e8ecf2); } + +.storeHome { overflow-y: auto; padding: 12px 16px 18px; flex: 1; } +.sectionLabel { + display: flex; align-items: center; gap: 6px; + font-weight: 700; font-size: 14px; color: var(--text, #e8ecf2); + margin-bottom: 10px; +} +.catGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} +.catTile { + display: flex; flex-direction: column; align-items: flex-start; gap: 4px; + padding: 16px; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.09); + border-radius: 12px; + cursor: pointer; + text-align: left; + transition: transform .1s, background .12s, border-color .12s; +} +.catTile:hover { + background: rgba(77,107,255,0.12); + border-color: var(--accent, #4d6bff); + transform: translateY(-2px); +} +.catTileIcon { color: var(--accent, #4d6bff); margin-bottom: 4px; } +.catTileLabel { font-weight: 700; font-size: 15px; color: var(--text, #e8ecf2); } +.catTileDesc { font-size: 11px; opacity: 0.7; line-height: 1.3; } + +.trendRow { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} +.trendCard { + position: relative; + display: flex; flex-direction: column; align-items: center; gap: 8px; + padding: 14px 8px; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.09); + border-radius: 12px; + cursor: pointer; + transition: transform .1s, border-color .12s; +} +.trendCard:hover { transform: translateY(-2px); border-color: var(--accent, #4d6bff); } +.trendIcon { + width: 100%; height: 70px; + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, rgba(77,107,255,0.22), rgba(54,213,122,0.16)); + border-radius: 8px; + color: var(--text, #e8ecf2); +} +.trendName { font-size: 12px; font-weight: 600; text-align: center; color: var(--text, #e8ecf2); } +.freeBadge { + position: absolute; top: 8px; right: 8px; + font-size: 9px; font-weight: 800; letter-spacing: 0.5px; + color: #36d57a; + background: rgba(54,213,122,0.14); + padding: 2px 6px; border-radius: 6px; +} + +.soon { + flex: 1; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 10px; padding: 40px; + color: var(--text-dim, #9aa3b2); text-align: center; +} +.soonTitle { font-size: 18px; font-weight: 700; color: var(--text, #e8ecf2); } +.soonText { font-size: 13px; max-width: 360px; opacity: 0.75; } + +.tips { + overflow-y: auto; padding: 16px 22px; flex: 1; + color: var(--text, #e8ecf2); line-height: 1.55; +} +.tips h3 { margin: 4px 0 12px; font-size: 17px; } +.tips ul { margin: 0 0 14px; padding-left: 18px; } +.tips li { margin-bottom: 9px; font-size: 13px; } +.tips b { color: var(--accent, #6f8bff); } diff --git a/src/editor/TopRibbon.jsx b/src/editor/TopRibbon.jsx index 5966394..94ed199 100644 --- a/src/editor/TopRibbon.jsx +++ b/src/editor/TopRibbon.jsx @@ -140,10 +140,12 @@ const Dropdown = ({ trigger, children }) => { */ const TABS = [ - { id: 'home', label: 'Главная', iconName: 'home' }, - { id: 'model', label: 'Модель', iconName: 'wrench' }, - { id: 'test', label: 'Игра', iconName: 'gamepad' }, - { id: 'view', label: 'Вид', iconName: 'eye' }, + { id: 'home', label: 'Главная', iconName: 'home' }, + // Вкладка-редактор СВОИХ воксельных моделей (создание ассета). + // Каталог готовых моделей/механик теперь в Toolbox (кнопка на «Главной»). + { id: 'model', label: 'Редактор моделей', iconName: 'wrench' }, + { id: 'test', label: 'Игра', iconName: 'gamepad' }, + { id: 'view', label: 'Вид', iconName: 'eye' }, ]; const SNAP_OPTIONS = [ @@ -329,9 +331,9 @@ const TopRibbon = (props) => { title="Параметрическая фигура (куб/сфера/...)" /> onToolChange('model')} + iconName="box" label="Toolbox" + onClick={onOpenStandardModels} + title="Библиотека: 3D-объекты, готовые механики, эффекты (как Creator Store)" />