feat(studio): задача 17 — Toolbox переработан под Roblox Creator Store

Единый 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 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 01:33:39 +03:00
parent d62739d709
commit 5e1a0edf9b
3 changed files with 375 additions and 169 deletions

View File

@ -282,6 +282,13 @@ const ToolboxModal = ({
initialSection = 'standard', initialSection = 'standard',
}) => { }) => {
// Корневой раздел: 'standard' | 'mine' | 'community' // Корневой раздел: '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 [section, setSection] = useState('standard');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [category, setCategory] = useState('all'); // для 'standard' const [category, setCategory] = useState('all'); // для 'standard'
@ -297,6 +304,9 @@ const ToolboxModal = ({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setSearch(''); setSearch('');
// initialSection маппится в новую структуру: mine inventory.
if (initialSection === 'mine') { setView('inventory'); setStoreCat(null); }
else { setView('store'); setStoreCat(null); }
setSection(initialSection || 'standard'); setSection(initialSection || 'standard');
setCategory('all'); setCategory('all');
setUserKind('all'); setUserKind('all');
@ -307,13 +317,30 @@ const ToolboxModal = ({
} }
}, [open, initialSection]); }, [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(() => { useEffect(() => {
if (!open) return; 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); window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]); }, [open, onClose, view, storeCat]);
// Lazy-load моих моделей при переключении на 'mine' // Lazy-load моих моделей при переключении на 'mine'
useEffect(() => { useEffect(() => {
@ -438,6 +465,27 @@ const ToolboxModal = ({
? (myModels?.length || 0) ? (myModels?.length || 0)
: (communityModels?.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; if (!open) return null;
// Обработчик выбора пользовательской модели пока stub. // Обработчик выбора пользовательской модели пока stub.
@ -503,67 +551,63 @@ const ToolboxModal = ({
return ( return (
<div className={cl.overlay} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}> <div className={cl.overlay} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
<div className={cl.modal}> <div className={cl.modal}>
{/* === Шапка с 4 верхними вкладками (как Roblox Creator Store) === */}
<header className={cl.header}> <header className={cl.header}>
<h2 className={cl.title} style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <h2 className={cl.title} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icon name="box" size={20} /> Тулбокс библиотека объектов <Icon name="box" size={20} /> Toolbox
</h2> </h2>
<div className={cl.headerInfo}>
{section === 'standard'
? `Показано ${visibleCount} из ${totalForSection}`
: section === 'gameplay'
? `${kitsFiltered.length} готовых механик`
: (myModels === null && section === 'mine') || (communityModels === null && section === 'community')
? '...'
: `${visibleCount} из ${totalForSection}`}
</div>
<button className={cl.closeBtn} onClick={onClose} title="Закрыть (Esc)"> <button className={cl.closeBtn} onClick={onClose} title="Закрыть (Esc)">
<Icon name="close" size={14} /> <Icon name="close" size={14} />
</button> </button>
</header> </header>
{/* Раздел: Стандартные / Мои / Сообщество */} <div className={cl.topTabs}>
<div className={cl.sectionTabs}> {[
<button { id: 'store', label: 'Магазин', icon: 'box' },
className={`${cl.sectionTab} ${section === 'standard' ? cl.sectionTabActive : ''}`} { id: 'inventory', label: 'Инвентарь', icon: 'grid' },
onClick={() => setSection('standard')} { id: 'recent', label: 'Недавние', icon: 'clock' },
> { id: 'tips', label: 'Советы', icon: 'bulb' },
<Icon name="library" size={15} /> Стандартные ].map(t => (
</button> <button
<button key={t.id}
className={`${cl.sectionTab} ${section === 'mine' ? cl.sectionTabActive : ''}`} className={`${cl.topTab} ${view === t.id ? cl.topTabActive : ''}`}
onClick={() => setSection('mine')} onClick={() => { setView(t.id); setStoreCat(null); setSearch(''); }}
> title={t.label}
<Icon name="user" size={15} /> Мои модели >
</button> <Icon name={t.icon} size={18} />
<button <span>{t.label}</span>
className={`${cl.sectionTab} ${section === 'community' ? cl.sectionTabActive : ''}`} </button>
onClick={() => setSection('community')} ))}
>
<Icon name="globe" size={15} /> Сообщество
</button>
<button
className={`${cl.sectionTab} ${section === 'gameplay' ? cl.sectionTabActive : ''}`}
onClick={() => setSection('gameplay')}
>
<Icon name="zap" size={15} /> Готовые механики
</button>
</div> </div>
<div className={cl.searchBar}> {/* Поиск — скрыт только на главном экране магазина и в советах */}
<input {!(view === 'store' && !storeCat) && view !== 'tips' && (
type="text" <div className={cl.searchBar}>
className={cl.searchInput} <input
placeholder={section === 'standard' type="text"
? 'Поиск по названию, категории...' className={cl.searchInput}
: 'Поиск по названию модели или автору...'} placeholder="Поиск по названию..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
autoFocus autoFocus
/> />
</div> </div>
)}
{/* Подкатегории зависят от раздела */} {/* Хлебные крошки/назад при открытой категории магазина */}
{section === 'standard' && ( {view === 'store' && storeCat && (
<div className={cl.breadcrumb}>
<button className={cl.backBtn} onClick={() => setStoreCat(null)}>
<Icon name="arrow-left" size={14} /> Категории
</button>
<span className={cl.crumbCurrent}>
{(STORE_CATEGORIES.find(c => c.id === storeCat) || {}).label}
</span>
</div>
)}
{/* Подкатегории standard (3D) */}
{view === 'store' && storeCat === '3d' && (
<div className={cl.categoryTabs}> <div className={cl.categoryTabs}>
{standardCategoriesWithCount.map(c => ( {standardCategoriesWithCount.map(c => (
<button <button
@ -578,151 +622,180 @@ const ToolboxModal = ({
))} ))}
</div> </div>
)} )}
{/* Подкатегории механик */}
{section === 'gameplay' && ( {view === 'store' && storeCat === 'gameplay' && (
<div className={cl.categoryTabs}> <div className={cl.categoryTabs}>
{KIT_CATEGORIES.map(c => ( {KIT_CATEGORIES.map(c => (
<button <button
key={c.id} key={c.id}
className={`${cl.categoryTab} ${kitCat === c.id ? cl.categoryTabActive : ''}`} className={`${cl.categoryTab} ${kitCat === c.id ? cl.categoryTabActive : ''}`}
onClick={() => setKitCat(c.id)} onClick={() => setKitCat(c.id)}
> >{c.label}</button>
{c.label} ))}
</div>
)}
{/* Фильтр kind для инвентаря */}
{view === 'inventory' && (
<div className={cl.categoryTabs}>
<button className={`${cl.categoryTab} ${userKind === 'all' ? cl.categoryTabActive : ''}`} onClick={() => setUserKind('all')}><Icon name="library" size={14} /> Все</button>
<button className={`${cl.categoryTab} ${userKind === 'voxel' ? cl.categoryTabActive : ''}`} onClick={() => setUserKind('voxel')}><Icon name="square" size={14} /> Воксельные</button>
<button className={`${cl.categoryTab} ${userKind === 'smooth' ? cl.categoryTabActive : ''}`} onClick={() => setUserKind('smooth')}><Icon name="sphere" size={14} /> Гладкие</button>
</div>
)}
{/* ====================== КОНТЕНТ ====================== */}
{/* --- МАГАЗИН: главный экран (6 плиток + Trending) --- */}
{view === 'store' && !storeCat && (
<div className={cl.storeHome}>
<div className={cl.sectionLabel}>Категории</div>
<div className={cl.catGrid}>
{STORE_CATEGORIES.map(c => (
<button key={c.id} className={cl.catTile} onClick={() => openStoreCategory(c.id)}>
<div className={cl.catTileIcon}><Icon name={c.icon} size={26} /></div>
<div className={cl.catTileLabel}>{c.label}</div>
<div className={cl.catTileDesc}>{c.desc}</div>
</button>
))}
</div>
<div className={cl.sectionLabel} style={{ marginTop: 18 }}>
<Icon name="trending" size={15} /> Популярное
</div>
<div className={cl.trendRow}>
{TRENDING.map(kit => (
<button key={kit.id} className={cl.trendCard}
onClick={() => { onPick('kit:' + kit.id); onClose(); }} title={kit.desc}>
<div className={cl.trendIcon}><Icon name={kit.icon || 'zap'} size={28} /></div>
<div className={cl.trendName}>{kit.name}</div>
<div className={cl.freeBadge}>FREE</div>
</button>
))}
</div>
</div>
)}
{/* --- МАГАЗИН: категория 3D-объекты --- */}
{view === 'store' && storeCat === '3d' && (
<div className={cl.grid}>
{standardFiltered.length === 0
? <div className={cl.empty}>Ничего не найдено</div>
: standardFiltered.map(m => (
<ThumbCard key={m.id} model={m} active={activeId === m.id}
onPick={() => { onPick(m.id); onClose(); }} />
))}
</div>
)}
{/* --- МАГАЗИН: Эффекты --- */}
{view === 'store' && storeCat === 'fx' && (
<div className={cl.grid}>
{FX_ITEMS.filter(f => !search.trim() || f.name.toLowerCase().includes(search.trim().toLowerCase())).map(f => (
<button key={f.id} type="button" className={cl.card} style={{ textAlign: 'left' }}
onClick={() => { onPick('primitive:' + f.id); onClose(); }} title={f.desc}>
<div className={cl.cardIconWrap}>
<div className={cl.cardIconPlaceholder} style={{ background: 'linear-gradient(135deg, rgba(255,90,176,0.22), rgba(255,210,58,0.18))' }}>
<Icon name={f.icon} size={30} />
</div>
</div>
<div className={cl.cardName}>{f.name}</div>
<div className={cl.cardCat} style={{ fontSize: 11, opacity: 0.8 }}>{f.desc}</div>
</button> </button>
))} ))}
</div> </div>
)} )}
{(section === 'mine' || section === 'community') && ( {/* --- МАГАЗИН: Готовые механики --- */}
<div className={cl.categoryTabs}> {view === 'store' && storeCat === 'gameplay' && (
<button <div className={cl.grid}>
className={`${cl.categoryTab} ${userKind === 'all' ? cl.categoryTabActive : ''}`} {kitsFiltered.length === 0
onClick={() => setUserKind('all')} ? <div className={cl.empty}>Ничего не найдено</div>
> : kitsFiltered.map(kit => (
<Icon name="library" size={14} /> Все <button key={kit.id} type="button" className={cl.card} style={{ textAlign: 'left' }}
</button> onClick={() => { onPick('kit:' + kit.id); onClose(); }} title={kit.desc}>
<button <div className={cl.cardIconWrap}>
className={`${cl.categoryTab} ${userKind === 'voxel' ? cl.categoryTabActive : ''}`} <div className={cl.cardIconPlaceholder} style={{ background: 'linear-gradient(135deg, rgba(77,107,255,0.25), rgba(54,213,122,0.18))' }}>
onClick={() => setUserKind('voxel')} <Icon name={kit.icon || 'zap'} size={30} />
> </div>
<Icon name="square" size={14} /> Воксельные </div>
</button> <div className={cl.cardName}>{kit.name}</div>
<button <div className={cl.cardCat} style={{ fontSize: 11, opacity: 0.8, lineHeight: 1.3, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{kit.desc}</div>
className={`${cl.categoryTab} ${userKind === 'smooth' ? cl.categoryTabActive : ''}`} <div className={cl.freeBadge}>FREE</div>
onClick={() => setUserKind('smooth')} </button>
> ))}
<Icon name="sphere" size={14} /> Гладкие
</button>
</div> </div>
)} )}
{/* === Контент === */} {/* --- МАГАЗИН: 2D-картинки / Плагины / Аудио — пока «скоро» --- */}
<div className={cl.grid}> {view === 'store' && (storeCat === '2d' || storeCat === 'plugins' || storeCat === 'audio') && (
{section === 'standard' && ( <div className={cl.soon}>
standardFiltered.length === 0 ? ( <Icon name={storeCat === 'audio' ? 'sound' : storeCat === '2d' ? 'image' : 'puzzle'} size={42} />
<div className={cl.empty}>Ничего не найдено</div> <div className={cl.soonTitle}>Скоро будет</div>
) : ( <div className={cl.soonText}>
standardFiltered.map(m => ( {storeCat === '2d' && 'Иконки и текстуры для интерфейса появятся в следующем обновлении.'}
<ThumbCard {storeCat === 'plugins' && 'Плагины-расширения студии — в разработке (фаза T4).'}
key={m.id} {storeCat === 'audio' && 'Библиотека звуков и музыки — в разработке.'}
model={m} </div>
active={activeId === m.id} </div>
onPick={() => { onPick(m.id); onClose(); }} )}
/>
))
)
)}
{section === 'mine' && ( {/* --- ИНВЕНТАРЬ: мои модели --- */}
loading || myModels === null ? ( {view === 'inventory' && (
<div className={cl.grid}>
{loading || myModels === null ? (
<div className={cl.empty}> Загрузка...</div> <div className={cl.empty}> Загрузка...</div>
) : !userId ? ( ) : !userId ? (
<div className={cl.empty}> <div className={cl.empty}>Войдите в аккаунт, чтобы видеть свои модели</div>
Войдите в аккаунт, чтобы видеть свои модели
</div>
) : loadError ? ( ) : loadError ? (
<div className={cl.empty}>{loadError}</div> <div className={cl.empty}>{loadError}</div>
) : mineFiltered.length === 0 ? ( ) : mineFiltered.length === 0 ? (
<div className={cl.empty}> <div className={cl.empty}>
{myModels.length === 0 {myModels.length === 0
? 'У вас пока нет своих моделей. Создайте их во вкладке «Модель» → «Воксельная» или «Гладкая».' ? 'У вас пока нет своих моделей. Создайте их в воксельном редакторе.'
: 'Ничего не найдено по фильтру'} : 'Ничего не найдено по фильтру'}
</div> </div>
) : ( ) : (
mineFiltered.map(m => ( mineFiltered.map(m => (
<UserModelCard <UserModelCard key={m.id} model={m} active={activeId === userModelKey(m)}
key={m.id} onPick={() => handlePickUserModel(m)} isMine
model={m} onEdit={onEditUserModel} onSettings={onUserModelSettings} onDelete={onDeleteUserModel} />
active={activeId === userModelKey(m)}
onPick={() => handlePickUserModel(m)}
isMine
onEdit={onEditUserModel}
onSettings={onUserModelSettings}
onDelete={onDeleteUserModel}
/>
)) ))
) )}
)} </div>
)}
{section === 'community' && ( {/* --- НЕДАВНИЕ: сообщество (популярные модели сообщества) --- */}
loading || communityModels === null ? ( {view === 'recent' && (
<div className={cl.grid}>
{communityModels === null ? (
<div className={cl.empty}> Загрузка...</div> <div className={cl.empty}> Загрузка...</div>
) : loadError ? (
<div className={cl.empty}>{loadError}</div>
) : communityFiltered.length === 0 ? ( ) : communityFiltered.length === 0 ? (
<div className={cl.empty}> <div className={cl.empty}>Пока пусто. Используй ассеты они появятся здесь.</div>
{communityModels.length === 0
? 'Пока нет опубликованных моделей сообщества. Будь первым!'
: 'Ничего не найдено по фильтру'}
</div>
) : ( ) : (
communityFiltered.map(m => ( communityFiltered.map(m => (
<UserModelCard <UserModelCard key={m.id} model={m} active={activeId === userModelKey(m)}
key={m.id}
model={m}
active={activeId === userModelKey(m)}
onPick={() => handlePickUserModel(m)} onPick={() => handlePickUserModel(m)}
isMine={userId != null && m.user_id === userId} isMine={userId != null && m.user_id === userId}
onEdit={onEditUserModel} onEdit={onEditUserModel} onSettings={onUserModelSettings} onDelete={onDeleteUserModel}
onSettings={onUserModelSettings} showSocial onLike={handleLikeModel} />
onDelete={onDeleteUserModel}
showSocial
onLike={handleLikeModel}
/>
)) ))
) )}
)} </div>
)}
{section === 'gameplay' && ( {/* --- СОВЕТЫ --- */}
kitsFiltered.length === 0 ? ( {view === 'tips' && (
<div className={cl.empty}>Ничего не найдено</div> <div className={cl.tips}>
) : ( <h3>Как пользоваться Toolbox</h3>
kitsFiltered.map(kit => ( <ul>
<button <li><b>3D-объекты</b> 700+ готовых моделей: деревья, дома, мебель, персонажи. Клик объект появляется на сцене.</li>
key={kit.id} <li><b>Готовые механики</b> вставь поведение одним кликом: бег на Shift, смена дня/ночи, сундук с лутом, счётчик монет. Скрипт прикрепляется сам.</li>
type="button" <li><b>Эффекты</b> частицы, лучи, источники света, триггер-зоны.</li>
className={cl.card} <li><b>Инвентарь</b> твои воксельные модели, созданные в редакторе.</li>
onClick={() => { onPick('kit:' + kit.id); onClose(); }} <li>Жми на категорию, ищи через поиск, кликни ассет он добавится в проект.</li>
title={kit.desc} </ul>
style={{ textAlign: 'left' }} <p style={{ opacity: 0.7 }}>Собери целую игру, не написав ни строчки кода просто перетаскивая готовые механики.</p>
> </div>
<div className={cl.cardIconWrap}> )}
<div className={cl.cardIconPlaceholder} style={{
background: 'linear-gradient(135deg, rgba(77,107,255,0.25), rgba(54,213,122,0.18))',
}}>
<Icon name={kit.icon || 'zap'} size={30} />
</div>
</div>
<div className={cl.cardName}>{kit.name}</div>
<div className={cl.cardCat} style={{
fontSize: 11, opacity: 0.8, lineHeight: 1.3,
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>{kit.desc}</div>
</button>
))
)
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -447,3 +447,134 @@
font-size: 36px; font-size: 36px;
color: var(--text-dim); 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); }

View File

@ -140,10 +140,12 @@ const Dropdown = ({ trigger, children }) => {
*/ */
const TABS = [ const TABS = [
{ id: 'home', label: 'Главная', iconName: 'home' }, { id: 'home', label: 'Главная', iconName: 'home' },
{ id: 'model', label: 'Модель', iconName: 'wrench' }, // Вкладка-редактор СВОИХ воксельных моделей (создание ассета).
{ id: 'test', label: 'Игра', iconName: 'gamepad' }, // Каталог готовых моделей/механик теперь в Toolbox (кнопка на «Главной»).
{ id: 'view', label: 'Вид', iconName: 'eye' }, { id: 'model', label: 'Редактор моделей', iconName: 'wrench' },
{ id: 'test', label: 'Игра', iconName: 'gamepad' },
{ id: 'view', label: 'Вид', iconName: 'eye' },
]; ];
const SNAP_OPTIONS = [ const SNAP_OPTIONS = [
@ -329,9 +331,9 @@ const TopRibbon = (props) => {
title="Параметрическая фигура (куб/сфера/...)" title="Параметрическая фигура (куб/сфера/...)"
/> />
<RibbonBtn <RibbonBtn
iconName="trees" label="Модель" iconName="box" label="Toolbox"
active={activeTool === 'model'} onClick={onOpenStandardModels}
onClick={() => onToolChange('model')} title="Библиотека: 3D-объекты, готовые механики, эффекты (как Creator Store)"
/> />
<RibbonBtn <RibbonBtn
iconName="image" label="Интерфейс" iconName="image" label="Интерфейс"