feat: bootstrap ShaCraft cross-platform launcher
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
name: Cross-platform build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux x64
|
||||
os: ubuntu-22.04
|
||||
args: --bundles appimage,deb
|
||||
- name: Windows x64
|
||||
os: windows-latest
|
||||
args: --bundles nsis,msi
|
||||
- name: macOS Apple Silicon
|
||||
os: macos-14
|
||||
args: --target aarch64-apple-darwin --bundles dmg
|
||||
- name: macOS Intel
|
||||
os: macos-13
|
||||
args: --target x86_64-apple-darwin --bundles dmg
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: npm ci
|
||||
- run: npm run tauri:build -- ${{ matrix.args }}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
dist/
|
||||
src-tauri/target/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.sig
|
||||
@@ -0,0 +1,34 @@
|
||||
# ShaCraft Launcher
|
||||
|
||||
Кроссплатформенный лаунчер для сети Minecraft-серверов ShaCraft.
|
||||
|
||||
Сейчас реализовано:
|
||||
|
||||
- интерактивный интерфейс профилей и настроек;
|
||||
- Tauri 2 native shell с безопасной командой `native_host`;
|
||||
- адаптивное окно для Windows, Linux и macOS;
|
||||
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
|
||||
Java 21 и запуск Minecraft.
|
||||
|
||||
## Локальная разработка
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Для нативного приложения нужен Rust:
|
||||
|
||||
```bash
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
На Ubuntu для сборки Tauri также потребуются системные пакеты WebKit/GTK и
|
||||
DBus development headers. Их установка описана в официальной документации
|
||||
Tauri и требует прав администратора.
|
||||
|
||||
## Статус
|
||||
|
||||
Лаунчер пока не загружает игровые файлы и не запускает Minecraft. Следующий
|
||||
этап — спроектировать новый подписываемый манифест ShaCraft v2 и добавить
|
||||
Rust-менеджер профилей.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#101411" />
|
||||
<title>ShaCraft Launcher</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1334
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "shacraft-launcher-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@vitejs/plugin-react": "latest",
|
||||
"@tauri-apps/cli": "^2.9.4",
|
||||
"lucide-react": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"typescript": "latest",
|
||||
"vite": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.4"
|
||||
}
|
||||
}
|
||||
Generated
+4511
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "shacraft-launcher"
|
||||
version = "0.1.0"
|
||||
description = "ShaCraft Minecraft launcher"
|
||||
authors = ["ShaCraft"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "shacraft_launcher_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main-window",
|
||||
"description": "Minimal permissions for the ShaCraft main window.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NativeHost {
|
||||
platform: &'static str,
|
||||
data_dir: String,
|
||||
launcher_version: &'static str,
|
||||
}
|
||||
|
||||
/// Returns non-sensitive environment information needed by the interface.
|
||||
/// File access and child-process launching are deliberately not exposed yet.
|
||||
#[tauri::command]
|
||||
fn native_host(app: AppHandle) -> Result<NativeHost, String> {
|
||||
let data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?;
|
||||
|
||||
Ok(NativeHost {
|
||||
platform: std::env::consts::OS,
|
||||
data_dir: data_dir.display().to_string(),
|
||||
launcher_version: env!("CARGO_PKG_VERSION"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![native_host])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running ShaCraft Launcher");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
shacraft_launcher_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ShaCraft Launcher",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ru.shacraft.launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ShaCraft Launcher",
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
"minWidth": 1040,
|
||||
"minHeight": 680,
|
||||
"decorations": false,
|
||||
"transparent": false,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
+248
@@ -0,0 +1,248 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import {
|
||||
ChevronRight,
|
||||
Download,
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
Globe2,
|
||||
Library,
|
||||
MessageCircle,
|
||||
Minus,
|
||||
Newspaper,
|
||||
Play,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Square,
|
||||
Users,
|
||||
Wrench,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import logo from './assets/shacraft-logo.png'
|
||||
import './styles.css'
|
||||
|
||||
type Server = {
|
||||
id: string
|
||||
kicker: string
|
||||
name: string
|
||||
subtitle: string
|
||||
players: string
|
||||
version: string
|
||||
memory: string
|
||||
installed: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type NativeHost = {
|
||||
platform: string
|
||||
dataDir: string
|
||||
launcherVersion: string
|
||||
}
|
||||
|
||||
const servers: Server[] = [
|
||||
{
|
||||
id: 'aoc',
|
||||
kicker: 'Основная сборка',
|
||||
name: 'Aeronautics',
|
||||
subtitle: 'Строй корабли. Поднимай города в небо.',
|
||||
players: '7 / 20',
|
||||
version: '1.21.1 · NeoForge',
|
||||
memory: '6 ГБ',
|
||||
installed: true,
|
||||
},
|
||||
{
|
||||
id: 'create',
|
||||
kicker: 'На техобслуживании',
|
||||
name: 'Create',
|
||||
subtitle: 'Механизмы, фабрики и большие идеи.',
|
||||
players: 'Сервер остановлен',
|
||||
version: '1.21.1 · NeoForge',
|
||||
memory: '4 ГБ',
|
||||
installed: false,
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
function App() {
|
||||
const [selected, setSelected] = useState(servers[0])
|
||||
const [progress, setProgress] = useState<number | null>(null)
|
||||
const [ready, setReady] = useState(true)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [ram, setRam] = useState(6)
|
||||
const [nativeHost, setNativeHost] = useState<NativeHost | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === null) return
|
||||
if (progress >= 100) {
|
||||
const done = window.setTimeout(() => {
|
||||
setProgress(null)
|
||||
setReady(true)
|
||||
}, 650)
|
||||
return () => window.clearTimeout(done)
|
||||
}
|
||||
const timer = window.setTimeout(() => setProgress(Math.min(100, progress + 2)), 55)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [progress])
|
||||
|
||||
useEffect(() => {
|
||||
if (!('__TAURI_INTERNALS__' in window)) return
|
||||
invoke<NativeHost>('native_host').then(setNativeHost).catch(() => setNativeHost(null))
|
||||
}, [])
|
||||
|
||||
const repair = () => {
|
||||
if (selected.disabled) return
|
||||
setReady(false)
|
||||
setProgress(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header className="titlebar">
|
||||
<div className="brand">
|
||||
<img src={logo} alt="" />
|
||||
<span>ShaCraft</span>
|
||||
</div>
|
||||
<div className="titlebar-drag">{nativeHost ? `Лаунчер · ${nativeHost.platform}` : 'Лаунчер'}</div>
|
||||
<div className="window-actions" aria-label="Управление окном">
|
||||
<button aria-label="Свернуть"><Minus size={15} /></button>
|
||||
<button aria-label="Развернуть"><Square size={12} /></button>
|
||||
<button className="close" aria-label="Закрыть"><X size={15} /></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="workspace">
|
||||
<nav className="rail" aria-label="Основное меню">
|
||||
<div className="rail-main">
|
||||
<button className="rail-button active" aria-label="Сборки"><Library /></button>
|
||||
<button className="rail-button" aria-label="Новости"><Newspaper /></button>
|
||||
<button className="rail-button" aria-label="Сообщество"><MessageCircle /></button>
|
||||
</div>
|
||||
<button className="rail-button" aria-label="Настройки" onClick={() => setSettingsOpen(true)}>
|
||||
<Settings />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<aside className="library-panel">
|
||||
<div className="library-heading">
|
||||
<p>Сборки</p>
|
||||
<span>1 доступна</span>
|
||||
</div>
|
||||
<div className="server-list">
|
||||
{servers.map((server) => (
|
||||
<button
|
||||
key={server.id}
|
||||
className={`server-row ${selected.id === server.id ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setSelected(server)
|
||||
setReady(server.installed)
|
||||
setProgress(null)
|
||||
}}
|
||||
>
|
||||
<span className={`server-glyph ${server.id}`} aria-hidden="true">
|
||||
{server.id === 'aoc' ? 'A' : 'C'}
|
||||
</span>
|
||||
<span className="server-copy">
|
||||
<strong>{server.name}</strong>
|
||||
<small>{server.disabled ? 'На паузе' : 'Установлена'}</small>
|
||||
</span>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="account-chip">
|
||||
<span className="avatar">ES</span>
|
||||
<span><strong>Émile</strong><small>offline-профиль</small></span>
|
||||
<ChevronRight size={16} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className={`stage stage-${selected.id}`}>
|
||||
<div className="stage-top">
|
||||
<div className={`live-pill ${selected.disabled ? 'offline' : ''}`}>
|
||||
<span /> {selected.disabled ? 'Не в сети' : 'Сервер работает'}
|
||||
</div>
|
||||
<div className="players"><Users size={16} /> {selected.players}</div>
|
||||
</div>
|
||||
|
||||
<section className="hero-copy">
|
||||
<p>{selected.id === 'aoc' ? 'All of Create / сборка 2.5' : selected.kicker}</p>
|
||||
<h1>{selected.name}</h1>
|
||||
<h2>{selected.subtitle}</h2>
|
||||
<dl className="hero-meta">
|
||||
<div><dt>Состав</dt><dd>{selected.id === 'aoc' ? '250 модов' : '41 мод'}</dd></div>
|
||||
<div><dt>Загрузчик</dt><dd>{selected.id === 'aoc' ? 'NeoForge 21.1.248' : 'NeoForge 21.1.249'}</dd></div>
|
||||
<div><dt>Java</dt><dd>Версия 21</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="play-dock">
|
||||
<div className="build-state">
|
||||
{progress !== null ? (
|
||||
<>
|
||||
<span className="state-icon downloading"><Download size={19} /></span>
|
||||
<span>
|
||||
<strong>Проверяем сборку</strong>
|
||||
<small>Файлы и обновления · {progress}%</small>
|
||||
</span>
|
||||
</>
|
||||
) : selected.disabled ? (
|
||||
<>
|
||||
<span className="state-icon muted"><Wrench size={19} /></span>
|
||||
<span><strong>Техобслуживание</strong><small>Сообщим, когда сервер вернётся</small></span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="state-icon"><ShieldCheck size={19} /></span>
|
||||
<span><strong>{ready ? 'Сборка готова' : 'Требуется проверка'}</strong><small>Обновлено сегодня в 21:42</small></span>
|
||||
</>
|
||||
)}
|
||||
{progress !== null && <div className="progress-track"><i style={{ width: `${progress}%` }} /></div>}
|
||||
</div>
|
||||
|
||||
<div className="build-facts">
|
||||
<span><Globe2 size={15} /> {selected.version}</span>
|
||||
<span><Gauge size={15} /> {ram} ГБ памяти</span>
|
||||
</div>
|
||||
|
||||
<button className="repair-button" onClick={repair} disabled={progress !== null || selected.disabled} aria-label="Проверить файлы">
|
||||
<RotateCcw size={19} />
|
||||
</button>
|
||||
<button
|
||||
className="play-button"
|
||||
disabled={progress !== null || selected.disabled}
|
||||
onClick={() => !ready && repair()}
|
||||
>
|
||||
<Play size={21} fill="currentColor" />
|
||||
<span>{selected.disabled ? 'Недоступно' : progress !== null ? 'Обновление' : ready ? 'Играть' : 'Проверить'}</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div className={`drawer-backdrop ${settingsOpen ? 'visible' : ''}`} onClick={() => setSettingsOpen(false)} />
|
||||
<aside className={`settings-drawer ${settingsOpen ? 'open' : ''}`} aria-hidden={!settingsOpen}>
|
||||
<div className="drawer-title">
|
||||
<div><p>Настройки</p><h2>Игра</h2></div>
|
||||
<button onClick={() => setSettingsOpen(false)} aria-label="Закрыть настройки"><X /></button>
|
||||
</div>
|
||||
<label className="range-setting">
|
||||
<span><strong>Оперативная память</strong><b>{ram} ГБ</b></span>
|
||||
<input type="range" min="3" max="12" value={ram} onChange={(e) => setRam(Number(e.target.value))} />
|
||||
<small>Для Aeronautics рекомендуется 6 ГБ</small>
|
||||
</label>
|
||||
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>
|
||||
<button className="setting-row"><span><Wrench />Дополнительные параметры</span><ChevronRight /></button>
|
||||
<div className="drawer-note">
|
||||
{nativeHost ? `Данные лаунчера: ${nativeHost.dataDir}` : 'Java 21 будет управляться лаунчером автоматически.'}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode><App /></React.StrictMode>,
|
||||
)
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@500;600&display=swap');
|
||||
|
||||
:root {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
color: #eef2ed;
|
||||
background: #090c0a;
|
||||
font-synthesis: none;
|
||||
--ink: #090c0a;
|
||||
--panel: #111612;
|
||||
--panel-2: #171d18;
|
||||
--line: rgba(229, 240, 228, 0.1);
|
||||
--muted: #879087;
|
||||
--green: #77d17a;
|
||||
--green-dark: #275d31;
|
||||
--copper: #e58a47;
|
||||
--ice: #87cbd3;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
button, input { font: inherit; }
|
||||
button { color: inherit; }
|
||||
body { margin: 0; min-width: 1040px; min-height: 680px; overflow: hidden; }
|
||||
button:focus-visible, input:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
|
||||
|
||||
.app-shell {
|
||||
height: 100vh;
|
||||
min-height: 680px;
|
||||
background: var(--ink);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #0c100d;
|
||||
user-select: none;
|
||||
}
|
||||
.brand { width: 262px; display: flex; align-items: center; gap: 9px; padding-left: 16px; font-weight: 800; letter-spacing: -0.02em; }
|
||||
.brand img { width: 25px; height: 25px; image-rendering: auto; }
|
||||
.brand span::first-letter { color: white; }
|
||||
.titlebar-drag { flex: 1; text-align: center; color: #626b63; font-size: 12px; }
|
||||
.window-actions { display: flex; height: 100%; }
|
||||
.window-actions button { width: 46px; border: 0; background: transparent; display: grid; place-items: center; color: #7e8780; }
|
||||
.window-actions button:hover { background: #1a201b; color: white; }
|
||||
.window-actions .close:hover { background: #a93939; }
|
||||
|
||||
.workspace { height: calc(100vh - 48px); display: grid; grid-template-columns: 64px 250px 1fr; }
|
||||
.rail { background: #0c100d; border-right: 1px solid var(--line); padding: 18px 10px 14px; display: flex; flex-direction: column; justify-content: space-between; }
|
||||
.rail-main { display: grid; gap: 8px; }
|
||||
.rail-button { border: 0; width: 44px; height: 44px; border-radius: 5px; display: grid; place-items: center; background: transparent; color: #69716a; cursor: pointer; }
|
||||
.rail-button svg { width: 20px; }
|
||||
.rail-button:hover { color: #cfd6cf; background: #171c18; }
|
||||
.rail-button.active { color: var(--green); background: #16251a; box-shadow: inset 2px 0 0 var(--green); }
|
||||
|
||||
.library-panel { background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; padding: 26px 14px 14px; }
|
||||
.library-heading { padding: 0 10px 19px; display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.library-heading p { margin: 0; font-weight: 750; font-size: 16px; }
|
||||
.library-heading span { color: #687069; font-size: 11px; }
|
||||
.server-list { display: grid; gap: 5px; }
|
||||
.server-row { width: 100%; border: 0; border-left: 2px solid transparent; background: transparent; padding: 10px 10px 10px 12px; border-radius: 3px; display: grid; grid-template-columns: 42px 1fr 16px; gap: 10px; align-items: center; text-align: left; cursor: pointer; }
|
||||
.server-row > svg { color: #555e56; }
|
||||
.server-row:hover { background: #171c18; }
|
||||
.server-row.selected { background: linear-gradient(90deg, rgba(229,138,71,.1), rgba(229,138,71,.025)); border-left-color: var(--copper); }
|
||||
.server-row.selected > svg { color: var(--copper); }
|
||||
.server-glyph { width: 42px; height: 42px; border-radius: 4px; display: grid; place-items: center; font-family: 'Unbounded'; font-size: 15px; color: #fff0e5; background: #9b5c31; box-shadow: inset 0 0 0 1px rgba(255,255,255,.1); }
|
||||
.server-glyph.create { color: #dae8dc; background: linear-gradient(145deg, #293b2c, #58755b); filter: saturate(.65); }
|
||||
.server-copy { min-width: 0; display: grid; }
|
||||
.server-copy strong { font-size: 13px; font-weight: 700; }
|
||||
.server-copy small { font-size: 10px; color: var(--green); margin-top: 2px; }
|
||||
.server-row:not(.selected) .server-copy small { color: #707871; }
|
||||
.account-chip { margin-top: auto; padding: 10px; border-top: 1px solid var(--line); display: grid; grid-template-columns: 34px 1fr 16px; gap: 9px; align-items: center; }
|
||||
.account-chip .avatar { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: #243127; color: #bfe1c1; font-size: 11px; font-weight: 800; }
|
||||
.account-chip > span:nth-child(2) { display: grid; }
|
||||
.account-chip strong { font-size: 12px; }
|
||||
.account-chip small { color: #6d756e; font-size: 9px; }
|
||||
.account-chip svg { color: #555e56; }
|
||||
|
||||
.stage { position: relative; overflow: hidden; isolation: isolate; background: #16231b; }
|
||||
.stage::before { content: ''; position: absolute; inset: 0; z-index: -5; background:
|
||||
radial-gradient(ellipse 64% 78% at 86% 38%, rgba(237, 126, 50, .34), transparent 69%),
|
||||
radial-gradient(ellipse 58% 72% at 55% 34%, rgba(90, 163, 104, .2), transparent 72%),
|
||||
linear-gradient(112deg, #0b130e 0%, #11291d 39%, #28301f 66%, #512713 100%);
|
||||
}
|
||||
.stage::after { content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none; background: linear-gradient(180deg, transparent 45%, rgba(5,8,6,.9) 100%); }
|
||||
.stage-create::before { filter: grayscale(.55); background: linear-gradient(110deg, #152019, #27372b 55%, #151a16); }
|
||||
.stage-top { display: flex; justify-content: flex-end; align-items: center; gap: 12px; padding: 22px 26px; }
|
||||
.live-pill, .players { height: 28px; padding: 0 0 0 12px; border: 0; border-left: 1px solid rgba(255,255,255,.16); border-radius: 0; background: transparent; display: flex; align-items: center; gap: 7px; font-size: 11px; color: #c0c8c0; }
|
||||
.live-pill span { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 4px rgba(119,209,122,.1), 0 0 12px rgba(119,209,122,.55); }
|
||||
.live-pill.offline span { background: #7d8580; box-shadow: none; }
|
||||
.players { color: #9ea69f; }
|
||||
.hero-copy { position: absolute; left: 7%; top: 43%; transform: translateY(-50%); max-width: 680px; }
|
||||
.hero-copy p { margin: 0 0 15px; color: var(--copper); font-size: 11px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
||||
.hero-copy p::before { content: ''; width: 26px; height: 2px; background: var(--copper); }
|
||||
.hero-copy h1 { font-family: 'Unbounded', sans-serif; font-size: clamp(40px, 5vw, 66px); line-height: 1; letter-spacing: -.045em; margin: 0; text-shadow: 0 8px 36px rgba(0,0,0,.3); }
|
||||
.hero-copy h2 { max-width: 440px; font-size: 15px; line-height: 1.5; font-weight: 500; color: #aeb7af; margin: 16px 0 0; }
|
||||
.stage-create .hero-copy p { color: #9aa49c; }
|
||||
.hero-meta { display: flex; gap: 0; margin: 36px 0 0; padding: 0; }
|
||||
.hero-meta div { min-width: 130px; padding: 0 26px; border-left: 1px solid rgba(235,243,234,.14); }
|
||||
.hero-meta div:first-child { padding-left: 0; border-left: 0; }
|
||||
.hero-meta dt { color: #6e786f; font-size: 9px; margin-bottom: 5px; }
|
||||
.hero-meta dd { color: #d9ded9; font-size: 11px; font-weight: 650; margin: 0; }
|
||||
|
||||
.play-dock { position: absolute; left: 24px; right: 24px; bottom: 24px; min-height: 82px; border: 1px solid rgba(231,241,230,.13); border-top: 2px solid rgba(229,138,71,.48); background: rgba(12,17,13,.92); backdrop-filter: blur(22px); border-radius: 6px; padding: 13px 14px 13px 16px; display: grid; grid-template-columns: minmax(245px,1fr) auto 44px 184px; gap: 14px; align-items: center; box-shadow: 0 18px 55px rgba(0,0,0,.38); }
|
||||
.build-state { min-width: 0; display: grid; grid-template-columns: 42px 1fr; align-items: center; gap: 11px; position: relative; }
|
||||
.state-icon { grid-row: span 2; width: 42px; height: 42px; display: grid; place-items: center; border-radius: 4px; background: #17311d; color: var(--green); }
|
||||
.state-icon.downloading { color: var(--ice); background: #172b2d; }
|
||||
.state-icon.muted { color: #8c948e; background: #252b26; }
|
||||
.build-state > span:nth-child(2) { display: grid; }
|
||||
.build-state strong { font-size: 12px; }
|
||||
.build-state small { color: #737c74; font-size: 10px; margin-top: 3px; }
|
||||
.progress-track { position: absolute; left: 53px; right: 0; height: 2px; bottom: -5px; background: #283029; border-radius: 2px; overflow: hidden; }
|
||||
.progress-track i { display: block; height: 100%; background: var(--ice); transition: width .08s linear; }
|
||||
.build-facts { display: flex; gap: 17px; padding: 0 4px; }
|
||||
.build-facts span { color: #8c958d; display: flex; align-items: center; gap: 6px; font-size: 10px; white-space: nowrap; }
|
||||
.repair-button, .play-button { border: 0; cursor: pointer; height: 50px; border-radius: 4px; }
|
||||
.repair-button { width: 44px; display: grid; place-items: center; background: #252c26; color: #9ca59d; }
|
||||
.repair-button:hover { color: white; background: #303831; }
|
||||
.play-button { background: var(--green); color: #0b160d; display: flex; justify-content: center; align-items: center; gap: 10px; font-weight: 800; box-shadow: none; }
|
||||
.play-button:hover { background: #8bdd8e; transform: translateY(-1px); }
|
||||
.play-button:active { transform: translateY(0); }
|
||||
.repair-button:disabled, .play-button:disabled { opacity: .45; cursor: default; transform: none; }
|
||||
|
||||
.drawer-backdrop { position: fixed; inset: 48px 0 0; background: rgba(0,0,0,.44); opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 20; }
|
||||
.drawer-backdrop.visible { opacity: 1; pointer-events: auto; }
|
||||
.settings-drawer { position: fixed; top: 48px; right: 0; bottom: 0; width: 390px; background: #121713; border-left: 1px solid var(--line); z-index: 21; padding: 28px; transform: translateX(100%); transition: transform .24s cubic-bezier(.2,.8,.2,1); box-shadow: -30px 0 70px rgba(0,0,0,.35); }
|
||||
.settings-drawer.open { transform: translateX(0); }
|
||||
.drawer-title { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 34px; }
|
||||
.drawer-title p { color: #737c74; font-size: 11px; margin: 0 0 5px; }
|
||||
.drawer-title h2 { margin: 0; font-family: 'Unbounded'; font-size: 25px; }
|
||||
.drawer-title button { border: 0; background: #222923; width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; cursor: pointer; }
|
||||
.drawer-title button svg { width: 18px; }
|
||||
.range-setting { display: grid; background: #191f1a; padding: 18px; border-radius: 13px; margin-bottom: 12px; }
|
||||
.range-setting > span { display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
|
||||
.range-setting b { color: var(--green); }
|
||||
.range-setting input { accent-color: var(--green); margin: 18px 0 8px; }
|
||||
.range-setting small { color: #707971; font-size: 10px; }
|
||||
.setting-row { width: 100%; height: 55px; border: 0; border-bottom: 1px solid var(--line); background: transparent; display: flex; align-items: center; justify-content: space-between; cursor: pointer; color: #abb3ac; }
|
||||
.setting-row span { display: flex; align-items: center; gap: 11px; font-size: 12px; }
|
||||
.setting-row svg { width: 17px; color: #677068; }
|
||||
.setting-row:hover { color: white; }
|
||||
.drawer-note { position: absolute; left: 28px; right: 28px; bottom: 26px; padding: 13px 15px; background: #17231a; color: #8fb193; border-radius: 10px; font-size: 10px; }
|
||||
|
||||
@media (max-width: 1160px) {
|
||||
.workspace { grid-template-columns: 58px 224px 1fr; }
|
||||
.brand { width: 240px; }
|
||||
.build-facts span:first-child { display: none; }
|
||||
.play-dock { grid-template-columns: minmax(220px,1fr) auto 44px 160px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
|
||||
}
|
||||
Reference in New Issue
Block a user