Open-source веб-студия для создания игр Рублокса, двойная лицензия AGPL-3.0 + Коммерческая. Главное: - Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16 - Самодостаточный движок ~28к строк (66 файлов): BlockManager, TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов - Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco) - Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn) - Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt) - 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.) - Конфигурируемый бэкенд через VITE_API_BASE — работает со staging (dev-api.rublox.pro) без настройки - Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка - Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG - ESLint + Prettier + EditorConfig - Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md - Issue templates: bug_report, feature_request, security_disclosure Перед публикацией: - Все импорты из minecraftia заменены на локальные - Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env - Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо) - AdminKubikonModeration не публикуется (модерация — в team.rublox.pro) - 93 МБ ассетов public/kubikon-assets вынесены в .gitignore (раздаются через release artifact)
235 lines
8.2 KiB
JavaScript
235 lines
8.2 KiB
JavaScript
/**
|
||
* Рублокс — современная light-тема (Roblox/Discord/Apple Music style).
|
||
*
|
||
* Палитра построена вокруг логотипа A02 — синий #3357ff.
|
||
* Используется в KubikonFeed, KubikonGamePage и связанных модалках.
|
||
*/
|
||
|
||
export const KT = {
|
||
// === Фон ===
|
||
bg: '#f5f7fb',
|
||
bgPage: '#ffffff',
|
||
bgHover: '#f1f5f9',
|
||
bgMuted: '#f3f4f6',
|
||
bgSubtle: '#fafbfd',
|
||
|
||
// Стеклянный фон (для glassmorphism)
|
||
glass: 'rgba(255, 255, 255, 0.65)',
|
||
glassDark: 'rgba(255, 255, 255, 0.85)',
|
||
|
||
// Границы
|
||
border: '#e5e7eb',
|
||
borderStrong: '#cbd5e1',
|
||
borderSoft: '#eef2f7',
|
||
|
||
// === Текст ===
|
||
text: '#0f172a',
|
||
textSecondary:'#475569',
|
||
textMuted: '#94a3b8',
|
||
|
||
// === Акцент: Рублокс синий из логотипа A02 ===
|
||
accent: '#3357ff',
|
||
accentHover: '#2540d8',
|
||
accentSoft: '#e0e8ff',
|
||
accentDeep: '#1e2da5',
|
||
|
||
// === Дополнительные цвета бренда (для градиентов и hot-меток) ===
|
||
pink: '#ec4899',
|
||
violet: '#8b5cf6',
|
||
cyan: '#06b6d4',
|
||
lime: '#84cc16',
|
||
gold: '#f59e0b',
|
||
|
||
// === Семантика ===
|
||
danger: '#ef4444',
|
||
dangerLight: '#fef2f2',
|
||
warning: '#f59e0b',
|
||
warningLight: '#fffbeb',
|
||
success: '#10b981',
|
||
successLight: '#ecfdf5',
|
||
info: '#0ea5e9',
|
||
infoLight: '#f0f9ff',
|
||
|
||
// === Тени (Apple-style мягкие) ===
|
||
shadowSm: '0 1px 2px rgba(15, 23, 42, 0.04)',
|
||
shadow: '0 1px 3px rgba(15, 23, 42, 0.06), 0 2px 6px rgba(15, 23, 42, 0.04)',
|
||
shadowMd: '0 4px 16px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04)',
|
||
shadowLg: '0 12px 32px rgba(15, 23, 42, 0.12), 0 6px 12px rgba(15, 23, 42, 0.06)',
|
||
shadowXl: '0 24px 48px rgba(15, 23, 42, 0.16), 0 12px 24px rgba(15, 23, 42, 0.08)',
|
||
shadowGlow: '0 0 0 4px rgba(51, 87, 255, 0.18)',
|
||
shadowAccent: '0 8px 24px rgba(51, 87, 255, 0.32)',
|
||
|
||
// === Радиусы ===
|
||
radius: 10,
|
||
radiusLg: 14,
|
||
radiusXl: 20,
|
||
radius2xl: 28,
|
||
|
||
// === Шрифт ===
|
||
font: '"Roboto Condensed", system-ui, -apple-system, sans-serif',
|
||
|
||
// === Главные градиенты (для hero, кнопок, badges) ===
|
||
gradientBrand: 'linear-gradient(135deg, #3357ff 0%, #1e2da5 100%)',
|
||
gradientHot: 'linear-gradient(135deg, #ec4899 0%, #ef4444 50%, #f59e0b 100%)',
|
||
gradientCool: 'linear-gradient(135deg, #06b6d4 0%, #3357ff 100%)',
|
||
gradientPurple: 'linear-gradient(135deg, #8b5cf6 0%, #3357ff 100%)',
|
||
gradientHero: 'linear-gradient(135deg, #3357ff 0%, #6d28d9 50%, #ec4899 100%)',
|
||
};
|
||
|
||
|
||
/** Hover-состояние карточки игры (3D-tilt + lift). */
|
||
export const cardStyle = (hovered) => ({
|
||
background: KT.bgPage,
|
||
border: `1px solid ${hovered ? KT.accent : KT.border}`,
|
||
borderRadius: KT.radiusLg,
|
||
boxShadow: hovered ? KT.shadowLg : KT.shadow,
|
||
transform: hovered ? 'translateY(-6px) scale(1.02)' : 'translateY(0) scale(1)',
|
||
transition: 'all 280ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||
cursor: 'pointer',
|
||
overflow: 'hidden',
|
||
willChange: 'transform',
|
||
});
|
||
|
||
|
||
/** Кнопка-первичная: синий градиент, мягкая тень, при hover — приподнимается. */
|
||
export const buttonPrimary = (hovered) => ({
|
||
padding: '12px 22px',
|
||
background: hovered
|
||
? 'linear-gradient(135deg, #2540d8 0%, #1e2da5 100%)'
|
||
: KT.gradientBrand,
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: KT.radius,
|
||
fontSize: 14,
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
fontFamily: KT.font,
|
||
boxShadow: hovered
|
||
? '0 12px 28px rgba(51, 87, 255, 0.40)'
|
||
: '0 4px 12px rgba(51, 87, 255, 0.24)',
|
||
transform: hovered ? 'translateY(-2px) scale(1.02)' : 'translateY(0) scale(1)',
|
||
transition: 'all 200ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||
letterSpacing: 0.3,
|
||
});
|
||
|
||
|
||
/** Кнопка-вторичная (нейтральная белая). */
|
||
export const buttonSecondary = (hovered) => ({
|
||
padding: '10px 18px',
|
||
background: hovered ? KT.bgHover : KT.bgPage,
|
||
color: KT.text,
|
||
border: `1px solid ${hovered ? KT.borderStrong : KT.border}`,
|
||
borderRadius: KT.radius,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
fontFamily: KT.font,
|
||
transition: 'all 150ms ease',
|
||
});
|
||
|
||
|
||
/** Чип-фильтр (pill). */
|
||
export const chipStyle = (active) => ({
|
||
padding: '8px 16px',
|
||
background: active ? KT.accent : KT.bgPage,
|
||
color: active ? '#fff' : KT.textSecondary,
|
||
border: `1px solid ${active ? KT.accent : KT.border}`,
|
||
borderRadius: 999,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
fontFamily: KT.font,
|
||
transition: 'all 200ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||
whiteSpace: 'nowrap',
|
||
boxShadow: active ? '0 4px 12px rgba(51, 87, 255, 0.28)' : 'none',
|
||
transform: active ? 'translateY(-1px)' : 'translateY(0)',
|
||
});
|
||
|
||
|
||
/** Глобальные keyframes — один раз на страницу. */
|
||
export const KUBIKON_KEYFRAMES = `
|
||
@keyframes kubikonFadeIn {
|
||
from { opacity: 0; transform: translateY(12px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes kubikonFadeInScale {
|
||
from { opacity: 0; transform: scale(0.94); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
@keyframes kubikonShimmer {
|
||
0% { background-position: -200% 0; }
|
||
100% { background-position: 200% 0; }
|
||
}
|
||
@keyframes kubikonPulseGlow {
|
||
0%, 100% { box-shadow: 0 8px 24px rgba(51, 87, 255, 0.32); }
|
||
50% { box-shadow: 0 8px 24px rgba(51, 87, 255, 0.32),
|
||
0 0 0 12px rgba(51, 87, 255, 0.08); }
|
||
}
|
||
@keyframes kubikonHeroSlide {
|
||
from { opacity: 0; transform: translateY(24px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes kubikonGradientShift {
|
||
0%, 100% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
}
|
||
@keyframes kubikonFloat {
|
||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||
33% { transform: translateY(-12px) rotate(3deg); }
|
||
66% { transform: translateY(8px) rotate(-2deg); }
|
||
}
|
||
@keyframes kubikonSpin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
@keyframes kubikonHotPulse {
|
||
0%, 100% { transform: scale(1) rotate(-3deg); }
|
||
50% { transform: scale(1.08) rotate(-3deg); }
|
||
}
|
||
@keyframes kubikonFloatY {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-8px); }
|
||
}
|
||
@keyframes kubikonFadeInUp {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes kubikonPulseRing {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.55); }
|
||
50% { box-shadow: 0 0 0 14px rgba(255, 215, 0, 0); }
|
||
}
|
||
@keyframes kubikonGlow {
|
||
0%, 100% { filter: drop-shadow(0 0 8px currentColor); }
|
||
50% { filter: drop-shadow(0 0 24px currentColor); }
|
||
}
|
||
@keyframes kubikonRotateY {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
@keyframes kubikonShimmerText {
|
||
0% { background-position: 0% 50%; }
|
||
100% { background-position: 200% 50%; }
|
||
}
|
||
@keyframes kubikonBgPulse {
|
||
0%, 100% { opacity: 0.8; transform: scale(1); }
|
||
50% { opacity: 1; transform: scale(1.05); }
|
||
}
|
||
@keyframes kubikonParticleFloat {
|
||
0% { transform: translateY(0) translateX(0) rotate(0deg); opacity: 0; }
|
||
15% { opacity: 0.7; }
|
||
85% { opacity: 0.7; }
|
||
100% { transform: translateY(-120px) translateX(30px) rotate(180deg); opacity: 0; }
|
||
}
|
||
`;
|
||
|
||
|
||
/** Skeleton-блок с волной shimmer. */
|
||
export const skeletonStyle = (extra = {}) => ({
|
||
background:
|
||
`linear-gradient(90deg, ${KT.bgMuted} 0%, ${KT.bgHover} 50%, ${KT.bgMuted} 100%)`,
|
||
backgroundSize: '200% 100%',
|
||
animation: 'kubikonShimmer 1.6s ease-in-out infinite',
|
||
borderRadius: KT.radius,
|
||
...extra,
|
||
});
|