I Want To Heal 2 build v1.0.2
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
hydrateLocalSaveFromServer,
|
||||
loadSession,
|
||||
localCloudSave,
|
||||
logoutAccount,
|
||||
pushCloudSave,
|
||||
type AuthSession,
|
||||
} from './actionApi'
|
||||
import type { ActionMechanicConfig } from './actionBoss/actionEncounterConfig'
|
||||
import type { ActionCharacter } from './actionMode'
|
||||
import { ActionModeScreen } from './components/ActionModeScreen'
|
||||
import { AuthScreen } from './components/AuthScreen'
|
||||
|
||||
export function App() {
|
||||
const [session, setSession] = useState<AuthSession | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [message, setMessage] = useState('')
|
||||
const [screenKey, setScreenKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function start() {
|
||||
try {
|
||||
const nextSession = await loadSession()
|
||||
if (!nextSession.account) {
|
||||
if (!cancelled) setSession(nextSession)
|
||||
return
|
||||
}
|
||||
const syncStatus = await hydrateLocalSaveFromServer()
|
||||
if (!cancelled) {
|
||||
setSession(nextSession)
|
||||
setScreenKey((current) => current + 1)
|
||||
setMessage(syncStatus === 'loaded' ? 'Server save loaded.' : '')
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setMessage('Server unavailable. Sign in when it comes back online.')
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
start()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleAuthenticated(nextSession: AuthSession) {
|
||||
if (nextSession.account) {
|
||||
const syncStatus = await hydrateLocalSaveFromServer()
|
||||
setMessage(syncStatus === 'loaded' ? 'Server save loaded.' : 'Local save uploaded.')
|
||||
setScreenKey((current) => current + 1)
|
||||
}
|
||||
setSession(nextSession)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await logoutAccount()
|
||||
setSession({ account: null })
|
||||
setMessage('Signed out.')
|
||||
}
|
||||
|
||||
function syncCharacter(character: ActionCharacter) {
|
||||
if (!session?.account) return
|
||||
pushCloudSave({ ...localCloudSave(), character }).catch(() => null)
|
||||
}
|
||||
|
||||
function syncMechanics(mechanics: ActionMechanicConfig) {
|
||||
if (!session?.account) return
|
||||
pushCloudSave({ ...localCloudSave(), mechanics }).catch(() => null)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="auth-shell">
|
||||
<section className="auth-panel single">
|
||||
<div className="auth-card">
|
||||
<p className="eyebrow">Loading</p>
|
||||
<h1>Connecting</h1>
|
||||
<p className="auth-message">Checking server session.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session?.account) {
|
||||
return <AuthScreen onAuthenticated={handleAuthenticated} serverMessage={message} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionModeScreen
|
||||
key={screenKey}
|
||||
account={session.account}
|
||||
onCharacterSaved={syncCharacter}
|
||||
onLogout={handleLogout}
|
||||
onMechanicsSaved={syncMechanics}
|
||||
serverMessage={message}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
ACTION_SAVE_KEY,
|
||||
loadActionCharacter,
|
||||
saveActionCharacter,
|
||||
type ActionCharacter,
|
||||
} from './actionMode'
|
||||
import {
|
||||
ACTION_MECHANIC_CONFIG_KEY,
|
||||
loadActionMechanicConfig,
|
||||
saveActionMechanicConfig,
|
||||
type ActionMechanicConfig,
|
||||
} from './actionBoss/actionEncounterConfig'
|
||||
|
||||
const AUTH_TOKEN_KEY = 'i-want-to-heal-2:auth-token:v1'
|
||||
|
||||
export type AuthAccount = {
|
||||
id: string
|
||||
username: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export type AuthSession = {
|
||||
account: AuthAccount | null
|
||||
profile?: null
|
||||
token?: string
|
||||
}
|
||||
|
||||
export type ActionCloudSave = {
|
||||
character?: ActionCharacter
|
||||
mechanics?: ActionMechanicConfig
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
type SavedSlotResponse = {
|
||||
save: null | {
|
||||
save_json?: ActionCloudSave
|
||||
saveJson?: ActionCloudSave
|
||||
}
|
||||
}
|
||||
|
||||
function configuredBaseUrl(value: string | undefined) {
|
||||
return value ? value.replace(/\/+$/, '') : ''
|
||||
}
|
||||
|
||||
function apiBaseUrl() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const runtimeBase = (window as Window & { CAPACITOR_API_BASE_URL?: string }).CAPACITOR_API_BASE_URL
|
||||
if (runtimeBase) return configuredBaseUrl(runtimeBase)
|
||||
}
|
||||
return configuredBaseUrl(import.meta.env.VITE_API_BASE_URL)
|
||||
}
|
||||
|
||||
function readAuthToken() {
|
||||
return window.localStorage.getItem(AUTH_TOKEN_KEY) ?? ''
|
||||
}
|
||||
|
||||
function writeAuthToken(token: string) {
|
||||
window.localStorage.setItem(AUTH_TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export function clearAuthToken() {
|
||||
window.localStorage.removeItem(AUTH_TOKEN_KEY)
|
||||
}
|
||||
|
||||
export async function requestActionApiJson<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const baseUrl = apiBaseUrl()
|
||||
const headers = new Headers(init.headers)
|
||||
const token = readAuthToken()
|
||||
if (token && !headers.has('Authorization')) headers.set('Authorization', `Bearer ${token}`)
|
||||
const response = await fetch(baseUrl ? `${baseUrl}${path}` : path, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
})
|
||||
const body = await response.json().catch(() => ({}))
|
||||
if (!response.ok) throw new Error(body.error ?? 'Unable to reach the game server.')
|
||||
return body as T
|
||||
}
|
||||
|
||||
export async function loadSession() {
|
||||
const session = await requestActionApiJson<AuthSession>('/api/auth/session')
|
||||
if (session.token) writeAuthToken(session.token)
|
||||
return session
|
||||
}
|
||||
|
||||
export async function loginAccount(username: string, password: string) {
|
||||
const session = await requestActionApiJson<AuthSession>('/api/auth/login', {
|
||||
body: JSON.stringify({ username, password }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
if (session.token) writeAuthToken(session.token)
|
||||
return session
|
||||
}
|
||||
|
||||
export async function registerAccount(username: string, password: string, characterName: string) {
|
||||
const session = await requestActionApiJson<AuthSession>('/api/auth/register', {
|
||||
body: JSON.stringify({ username, password, characterName }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
if (session.token) writeAuthToken(session.token)
|
||||
return session
|
||||
}
|
||||
|
||||
export async function logoutAccount() {
|
||||
await requestActionApiJson('/api/auth/logout', { method: 'POST' }).catch(() => null)
|
||||
clearAuthToken()
|
||||
}
|
||||
|
||||
export function localCloudSave(): ActionCloudSave {
|
||||
return {
|
||||
character: loadActionCharacter(),
|
||||
mechanics: loadActionMechanicConfig(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCloudSave(save: ActionCloudSave) {
|
||||
if (save.character) saveActionCharacter(save.character)
|
||||
if (save.mechanics) saveActionMechanicConfig(save.mechanics)
|
||||
}
|
||||
|
||||
export async function loadCloudSave() {
|
||||
const result = await requestActionApiJson<SavedSlotResponse>('/api/save/default')
|
||||
return result.save?.save_json ?? result.save?.saveJson ?? null
|
||||
}
|
||||
|
||||
export async function pushCloudSave(save: ActionCloudSave = localCloudSave()) {
|
||||
return requestActionApiJson('/api/save/default', {
|
||||
body: JSON.stringify({
|
||||
clientUpdatedAt: save.updatedAt ?? new Date().toISOString(),
|
||||
save,
|
||||
saveVersion: 1,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
export async function hydrateLocalSaveFromServer() {
|
||||
const remote = await loadCloudSave()
|
||||
if (remote) {
|
||||
applyCloudSave(remote)
|
||||
return 'loaded'
|
||||
}
|
||||
if (window.localStorage.getItem(ACTION_SAVE_KEY) || window.localStorage.getItem(ACTION_MECHANIC_CONFIG_KEY)) {
|
||||
await pushCloudSave()
|
||||
return 'uploaded'
|
||||
}
|
||||
return 'empty'
|
||||
}
|
||||
+1
-1
@@ -128,7 +128,7 @@ export const ACTION_DIFFICULTY_TIERS: ActionDifficultyTier[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const ACTION_SAVE_KEY = 'i-want-to-heal:action-mode-save:v2'
|
||||
export const ACTION_SAVE_KEY = 'i-want-to-heal:action-mode-save:v2'
|
||||
const LEGACY_ACTION_SAVE_KEY = 'i-want-to-heal:action-mode-save:v1'
|
||||
const MAX_ACTION_LEVEL = 25
|
||||
export const BULLDROME_UPGRADE_COST = 5
|
||||
|
||||
@@ -31,9 +31,15 @@ import {
|
||||
type ActionRunReward,
|
||||
} from '../actionMode'
|
||||
import { BulldromeBossSlice } from './BulldromeBossSlice'
|
||||
import type { AuthAccount } from '../actionApi'
|
||||
|
||||
type ActionModeScreenProps = {
|
||||
account?: AuthAccount
|
||||
onCharacterSaved?: (character: ActionCharacter) => void
|
||||
onBack?: () => void
|
||||
onLogout?: () => void
|
||||
onMechanicsSaved?: (config: ActionMechanicConfig) => void
|
||||
serverMessage?: string
|
||||
}
|
||||
|
||||
type ActionHubTab = 'dungeons' | 'raids' | 'pvp' | 'roguelike' | 'customize' | 'settings'
|
||||
@@ -54,7 +60,14 @@ const ACTION_HUB_ITEMS: Array<{
|
||||
{ id: 'settings', label: 'Settings', glyph: 'S', description: 'Tune action mode controls.' },
|
||||
]
|
||||
|
||||
export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
export function ActionModeScreen({
|
||||
account,
|
||||
onBack,
|
||||
onCharacterSaved,
|
||||
onLogout,
|
||||
onMechanicsSaved,
|
||||
serverMessage = '',
|
||||
}: ActionModeScreenProps) {
|
||||
const [character, setCharacter] = useState<ActionCharacter>(() => loadActionCharacter())
|
||||
const [activeDifficulty, setActiveDifficulty] = useState<ActionDifficulty | null>(null)
|
||||
const [activeDungeonId, setActiveDungeonId] = useState<PlayableActionDungeonId>('bulldrome')
|
||||
@@ -67,7 +80,8 @@ export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
|
||||
useEffect(() => {
|
||||
saveActionCharacter(character)
|
||||
}, [character])
|
||||
onCharacterSaved?.(character)
|
||||
}, [character, onCharacterSaved])
|
||||
|
||||
if (activeDifficulty) {
|
||||
return (
|
||||
@@ -121,12 +135,18 @@ export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
<div className="action-heading-meta">
|
||||
<div className="action-character-strip">
|
||||
<strong>{character.name}</strong>
|
||||
{account && <small>@{account.username}</small>}
|
||||
<small>Healer</small>
|
||||
<small>Level {character.level}</small>
|
||||
<div className="header-xp" title={`${character.experience} action experience`}>
|
||||
<span style={{ width: `${progress.percent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
{onLogout && (
|
||||
<button className="back-button" onClick={onLogout} type="button">
|
||||
Logout
|
||||
</button>
|
||||
)}
|
||||
{(activeScreen !== 'menu' || onBack) && (
|
||||
<button
|
||||
className="back-button"
|
||||
@@ -161,7 +181,7 @@ export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{message && <p className="action-mode-message">{message}</p>}
|
||||
{(message || serverMessage) && <p className="action-mode-message">{message || serverMessage}</p>}
|
||||
|
||||
{activeScreen === 'dungeons' && (
|
||||
<DungeonsPanel
|
||||
@@ -201,7 +221,7 @@ export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
)}
|
||||
|
||||
{activeScreen === 'settings' && (
|
||||
<ActionMechanicsAdmin />
|
||||
<ActionMechanicsAdmin onConfigSaved={onMechanicsSaved} />
|
||||
)}
|
||||
|
||||
{activeScreen !== 'menu' && activeScreen !== 'dungeons' && activeScreen !== 'customize' && activeScreen !== 'settings' && (
|
||||
@@ -216,7 +236,7 @@ export function ActionModeScreen({ onBack }: ActionModeScreenProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function ActionMechanicsAdmin() {
|
||||
function ActionMechanicsAdmin({ onConfigSaved }: { onConfigSaved?: (config: ActionMechanicConfig) => void }) {
|
||||
const [config, setConfig] = useState<ActionMechanicConfig>(() => loadActionMechanicConfig())
|
||||
const [enemyPage, setEnemyPage] = useState(0)
|
||||
const [attackPageByEnemy, setAttackPageByEnemy] = useState<Partial<Record<EnemyKind, number>>>({})
|
||||
@@ -233,10 +253,13 @@ function ActionMechanicsAdmin() {
|
||||
}
|
||||
setConfig(next)
|
||||
saveActionMechanicConfig(next)
|
||||
onConfigSaved?.(next)
|
||||
}
|
||||
|
||||
function resetConfig() {
|
||||
setConfig(resetActionMechanicConfig())
|
||||
const next = resetActionMechanicConfig()
|
||||
setConfig(next)
|
||||
onConfigSaved?.(next)
|
||||
setEnemyPage(0)
|
||||
setAttackPageByEnemy({})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
loginAccount,
|
||||
registerAccount,
|
||||
type AuthSession,
|
||||
} from '../actionApi'
|
||||
|
||||
type AuthScreenProps = {
|
||||
onAuthenticated: (session: AuthSession) => void
|
||||
serverMessage?: string
|
||||
}
|
||||
|
||||
export function AuthScreen({ onAuthenticated, serverMessage = '' }: AuthScreenProps) {
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login')
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [characterName, setCharacterName] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
async function submit(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const session = mode === 'login'
|
||||
? await loginAccount(username, password)
|
||||
: await registerAccount(username, password, characterName)
|
||||
onAuthenticated(session)
|
||||
} catch (reason) {
|
||||
setMessage(reason instanceof Error ? reason.message : 'Unable to authenticate.')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="auth-shell">
|
||||
<section className="auth-panel">
|
||||
<div className="auth-brand">
|
||||
<p className="eyebrow">Action Healer</p>
|
||||
<h1>I Want To Heal 2</h1>
|
||||
<p>
|
||||
Sign in to sync saves to the server, keep progression across devices,
|
||||
and queue for PvP matchmaking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="auth-card">
|
||||
<div className="auth-tabs">
|
||||
<button
|
||||
className={mode === 'login' ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
setMode('login')
|
||||
setMessage('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
className={mode === 'register' ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
setMode('register')
|
||||
setMessage('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
autoComplete="username"
|
||||
maxLength={20}
|
||||
minLength={3}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
pattern="[A-Za-z0-9_]+"
|
||||
required
|
||||
value={username}
|
||||
/>
|
||||
</label>
|
||||
{mode === 'register' && (
|
||||
<label>
|
||||
Character Name
|
||||
<input
|
||||
autoComplete="nickname"
|
||||
maxLength={20}
|
||||
minLength={2}
|
||||
onChange={(event) => setCharacterName(event.target.value)}
|
||||
required
|
||||
value={characterName}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
|
||||
maxLength={128}
|
||||
minLength={10}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-button" disabled={busy} type="submit">
|
||||
{busy
|
||||
? 'Working...'
|
||||
: mode === 'login'
|
||||
? 'Enter'
|
||||
: 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className={`auth-message ${message ? 'error' : ''}`}>
|
||||
{message || serverMessage || (
|
||||
mode === 'register'
|
||||
? 'Creates an account and uploads the current local save if one exists.'
|
||||
: 'Sign in to load your server save.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ActionModeScreen } from './components/ActionModeScreen'
|
||||
import { App } from './App'
|
||||
import './styles.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ActionModeScreen />
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
+179
@@ -197,6 +197,122 @@ h2 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
align-items: center;
|
||||
background:
|
||||
radial-gradient(circle at 20% 0%, rgba(229, 185, 95, 0.08), transparent 26%),
|
||||
linear-gradient(180deg, #0d0f15 0%, #07080b 100%);
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
|
||||
margin: 0 auto;
|
||||
max-width: 980px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-panel.single {
|
||||
grid-template-columns: minmax(0, 520px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-brand,
|
||||
.auth-card {
|
||||
background: var(--panel);
|
||||
border: 3px solid #0c0d11;
|
||||
box-shadow: 7px 7px 0 #08090c;
|
||||
min-width: 0;
|
||||
outline: 2px solid var(--edge);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 16px;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.auth-brand h1 {
|
||||
font-size: clamp(20px, 3.4vw, 34px);
|
||||
}
|
||||
|
||||
.auth-brand > p:last-child {
|
||||
color: var(--muted);
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.auth-tabs {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.auth-tabs button {
|
||||
background: #17181e;
|
||||
border: 2px solid #090a0d;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
min-height: 42px;
|
||||
outline: 2px solid #3e3d47;
|
||||
}
|
||||
|
||||
.auth-tabs button.selected {
|
||||
color: var(--gold);
|
||||
outline-color: var(--gold);
|
||||
}
|
||||
|
||||
.auth-card form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.auth-card label {
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
gap: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.auth-card input {
|
||||
background: #111217;
|
||||
border: 2px solid #090a0d;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
outline: 2px solid #3e3d47;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-card input:focus {
|
||||
outline-color: var(--gold);
|
||||
}
|
||||
|
||||
.auth-message {
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
margin-top: 16px;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.auth-message.error {
|
||||
color: #ff8190;
|
||||
}
|
||||
|
||||
.action-run-reward-backdrop {
|
||||
align-items: center;
|
||||
background: rgba(5, 6, 9, 0.78);
|
||||
@@ -1617,6 +1733,61 @@ h2 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(0, 0.95fr) minmax(260px, 1.05fr);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.auth-brand,
|
||||
.auth-card {
|
||||
box-shadow: none;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.auth-brand h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.auth-brand > p:last-child,
|
||||
.auth-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-card form {
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.auth-tabs button {
|
||||
font-size: 6px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.auth-card label {
|
||||
font-size: 6px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.auth-card input {
|
||||
font-size: 12px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.auth-message {
|
||||
margin-top: 10px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.content-screen,
|
||||
.message-panel {
|
||||
box-shadow: none;
|
||||
@@ -2005,6 +2176,14 @@ h2 {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.screen-heading {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user