mirror of
https://github.com/System-End/spaces.git
synced 2026-04-19 16:38:24 +00:00
QoL stuff
This commit is contained in:
parent
3ab58eec3e
commit
0f17308379
13 changed files with 709 additions and 110 deletions
|
|
@ -30,4 +30,4 @@
|
||||||
* to avoid limiting type declarations.
|
* to avoid limiting type declarations.
|
||||||
*/
|
*/
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
import Auth from './lib/Auth.svelte';
|
import Auth from './lib/Auth.svelte';
|
||||||
import Dashboard from './lib/Dashboard.svelte';
|
import Dashboard from './lib/Dashboard.svelte';
|
||||||
import AdminPanel from './lib/AdminPanel.svelte';
|
import AdminPanel from './lib/AdminPanel.svelte';
|
||||||
|
import ThemeSwitcher from './lib/ThemeSwitcher.svelte';
|
||||||
import { API_BASE } from './config.js';
|
import { API_BASE } from './config.js';
|
||||||
|
import { applyTheme, currentTheme } from './stores/theme.js';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
let isAuthenticated = false;
|
let isAuthenticated = false;
|
||||||
let user = null;
|
let user = null;
|
||||||
|
|
@ -11,9 +14,11 @@
|
||||||
let showAdminPanel = false;
|
let showAdminPanel = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
applyTheme(get(currentTheme));
|
||||||
|
|
||||||
const storedAuth = localStorage.getItem('auth_token');
|
const storedAuth = localStorage.getItem('auth_token');
|
||||||
const storedUser = localStorage.getItem('user_data');
|
const storedUser = localStorage.getItem('user_data');
|
||||||
|
|
||||||
if (storedAuth && storedUser) {
|
if (storedAuth && storedUser) {
|
||||||
try {
|
try {
|
||||||
isAuthenticated = true;
|
isAuthenticated = true;
|
||||||
|
|
@ -89,6 +94,7 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{#if isAuthenticated && user}
|
{#if isAuthenticated && user}
|
||||||
|
<ThemeSwitcher />
|
||||||
{#if showAdminPanel && user.is_admin}
|
{#if showAdminPanel && user.is_admin}
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<button on:click={() => showAdminPanel = false}>Back to Dashboard</button>
|
<button on:click={() => showAdminPanel = false}>Back to Dashboard</button>
|
||||||
|
|
@ -120,20 +126,22 @@
|
||||||
|
|
||||||
.admin-header, .admin-link {
|
.admin-header, .admin-link {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #f8f9fa;
|
background-color: var(--snow);
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--smoke);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-header button, .admin-link button {
|
.admin-header button, .admin-link button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background-color: #007bff;
|
background-color: var(--blue);
|
||||||
color: white;
|
color: var(--white);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-header button:hover, .admin-link button:hover {
|
.admin-header button:hover, .admin-link button:hover {
|
||||||
background-color: #0056b3;
|
background-color: var(--cyan);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -96,3 +96,64 @@ button {
|
||||||
input, textarea, select {
|
input, textarea, select {
|
||||||
font-family: 'Phantom Sans', sans-serif;
|
font-family: 'Phantom Sans', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="darkGradient"] body {
|
||||||
|
background: linear-gradient(135deg, #0a0f1a 0%, #0f1a2a 25%, #1a2a3a 50%, #0f1a2a 75%, #0a0f1a 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="lgbtq"] body {
|
||||||
|
position: relative;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(228, 3, 3, 0.8) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(255, 140, 0, 0.8) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 40%, rgba(255, 237, 0, 0.8) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(0, 128, 38, 0.8) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(36, 64, 142, 0.8) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 60% 60%, rgba(115, 41, 130, 0.8) 0%, transparent 40%),
|
||||||
|
linear-gradient(135deg, #1a0616 0%, #2a0a26 100%);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: prideShift 30s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="lgbtq"] .dashboard,
|
||||||
|
[data-theme="lgbtq"] .auth-container,
|
||||||
|
[data-theme="lgbtq"] .create-form,
|
||||||
|
[data-theme="lgbtq"] .space-card,
|
||||||
|
[data-theme="lgbtq"] .theme-menu,
|
||||||
|
[data-theme="lgbtq"] .theme-toggle {
|
||||||
|
background: rgba(26, 6, 22, 0.7) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="lgbtq"] .auth-form-panel {
|
||||||
|
background: rgba(26, 6, 22, 0.8) !important;
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
-webkit-backdrop-filter: blur(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes prideShift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
background-position: 100% 0%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
background-position: 0% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
20
client/src/assets/flag.svg
Normal file
20
client/src/assets/flag.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-miterlimit="1.414"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="flag"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
fill="0"
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<path d="M10.953 5.034a1 1 0 0 0-1.225.707L4.034 26.992a1 1 0 1 0 1.932.517l5.694-21.25a1 1 0 0 0-.707-1.225zm2.107 9.005c.425-1.703.798-3.036 1.225-4.079.429-1.058.766-1.43.912-1.532a.216.216 0 0 0 .022-.023l.017.003c.131-.022.133-.021.353.073l.065.028c.584.23 1.492.826 2.826 2.076 1.584 1.462 3.173 2.338 4.36 2.738a9.906 9.906 0 0 0 2.045.4c-.312 1.161-.627 2.297-1.028 3.334-.405 1.061-.756 1.774-1.284 2.307-.385.41-.719.542-1.131.527-.519-.018-1.447-.289-2.901-1.37-1.746-1.291-3.25-2.073-4.327-2.514a17.61 17.61 0 0 0-1.498-.524c.08-.375.193-.838.344-1.444zm12.104-1.615a.522.522 0 0 1 0 0zm-13.21 2.816l.017.008a.08.08 0 0 1-.017-.008zm-.834-1.685c1.727-6.93 3.174-9.634 8.727-4.43 2.833 2.655 4.933 2.646 6.14 2.641 1.16-.005 1.494-.007.86 2.359-1.294 4.83-3.053 10.796-9.5 6-2.638-1.962-4.392-2.486-5.449-2.801-1.526-.456-1.599-.478-.778-3.769z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -35,7 +35,21 @@
|
||||||
message = 'Verification code sent to your email!';
|
message = 'Verification code sent to your email!';
|
||||||
mode = 'verify';
|
mode = 'verify';
|
||||||
} else {
|
} else {
|
||||||
error = data.message || 'Failed to send verification code';
|
if (response.status === 404 && mode === 'login') {
|
||||||
|
error = "We couldn't find your account. Let's create one!";
|
||||||
|
setTimeout(() => {
|
||||||
|
error = '';
|
||||||
|
authIntent = 'signup';
|
||||||
|
setTimeout(() => {
|
||||||
|
mode = 'signup';
|
||||||
|
}, 400);
|
||||||
|
setTimeout(() => {
|
||||||
|
displayMode = 'signup';
|
||||||
|
}, 800);
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
error = data.message || 'Failed to send verification code';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = ERROR_MESSAGES.NETWORK_ERROR;
|
error = ERROR_MESSAGES.NETWORK_ERROR;
|
||||||
|
|
@ -128,19 +142,21 @@
|
||||||
<img class="flag-banner" src="https://assets.hackclub.com/flag-orpheus-top.svg" alt="Hack Club"/>
|
<img class="flag-banner" src="https://assets.hackclub.com/flag-orpheus-top.svg" alt="Hack Club"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="auth-container" class:signup-mode={mode === 'signup'}>
|
<div class="auth-container" class:signup-mode={authIntent === 'signup'} class:verify-mode={mode === 'verify' && authIntent === 'login'}>
|
||||||
<div class="auth-panel auth-form-panel">
|
<div class="auth-panel auth-form-panel">
|
||||||
<div class="auth-form-content">
|
<div class="auth-form-content">
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<img class="auth-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
|
<img class="auth-logo" src="https://icons.hackclub.com/api/icons/ec3750/flag" alt="Hack Club" />
|
||||||
<h2 class="auth-title">{displayMode === 'signup' ? 'Join Hack Club Spaces' : 'Welcome Back'}</h2>
|
<h2 class="auth-title">{displayMode === 'signup' ? 'Join Hack Club Spaces' : 'Welcome Back'}</h2>
|
||||||
<p class="auth-subtitle">Lorem ipsum dolor sit amet</p>
|
<p class="auth-subtitle">Build amazing projects in the cloud</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if mode === 'login' || mode === 'signup'}
|
{#if mode === 'login' || mode === 'signup'}
|
||||||
<form on:submit|preventDefault={sendVerificationCode}>
|
<form on:submit|preventDefault={sendVerificationCode}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="email">Email</label>
|
<label class="form-label" for="email">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
id="email"
|
id="email"
|
||||||
|
|
@ -152,7 +168,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group username-field" class:show={mode === 'signup'}>
|
<div class="form-group username-field" class:show={mode === 'signup'}>
|
||||||
<label class="form-label" for="username">Username</label>
|
<label class="form-label" for="username">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
id="username"
|
id="username"
|
||||||
|
|
@ -172,7 +190,7 @@
|
||||||
<div class="success-message">{message}</div>
|
<div class="success-message">{message}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button class="primary-button" type="submit" disabled={loading || !email}>
|
<button class="primary-button" type="submit" disabled={loading || !email || (mode === 'signup' && !username)}>
|
||||||
{loading ? 'Sending...' : 'Send Verification Code'}
|
{loading ? 'Sending...' : 'Send Verification Code'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -184,12 +202,56 @@
|
||||||
Already have an account? <span class="auth-mode-link" on:click={() => switchMode('login')} on:keypress={(e) => e.key === 'Enter' && switchMode('login')} role="button" tabindex="0">Log in</span>
|
Already have an account? <span class="auth-mode-link" on:click={() => switchMode('login')} on:keypress={(e) => e.key === 'Enter' && switchMode('login')} role="button" tabindex="0">Log in</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if mode === 'verify'}
|
{/if}
|
||||||
<p class="info-message">Check your email for the verification code</p>
|
|
||||||
|
|
||||||
<form on:submit|preventDefault={authIntent === 'login' ? handleLogin : handleSignup}>
|
{#if authIntent === 'signup'}
|
||||||
|
<div class="verification-code-section" class:show={mode === 'verify'}>
|
||||||
|
<div class="verification-notice">
|
||||||
|
<p class="verification-title">Check your email</p>
|
||||||
|
<p class="verification-text">We sent a verification code to <strong>{email}</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={handleSignup}>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="code">
|
||||||
|
Verification Code
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="form-input"
|
||||||
|
id="code"
|
||||||
|
type="text"
|
||||||
|
bind:value={verificationCode}
|
||||||
|
required
|
||||||
|
placeholder="Enter code from email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-message">{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
|
||||||
|
{loading ? 'Verifying...' : 'Complete Sign Up'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="secondary-button" type="button" on:click={sendVerificationCode}>
|
||||||
|
Resend Code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if mode === 'verify' && authIntent === 'login'}
|
||||||
|
<div class="verification-notice">
|
||||||
|
<p class="verification-title">Check your email</p>
|
||||||
|
<p class="verification-text">We sent a verification code to <strong>{email}</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={handleLogin}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="code">Verification Code</label>
|
<label class="form-label" for="code">
|
||||||
|
Verification Code
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
id="code"
|
id="code"
|
||||||
|
|
@ -205,10 +267,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
|
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
|
||||||
{loading ? 'Verifying...' : authIntent === 'signup' ? 'Complete Sign Up' : 'Login'}
|
{loading ? 'Verifying...' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="secondary-button" type="button" on:click={() => switchMode(mode)}>
|
<button class="secondary-button" type="button" on:click={() => switchMode('login')}>
|
||||||
Resend Code
|
Resend Code
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
||||||
import '../styles/dashboard.css';
|
import '../styles/dashboard.css';
|
||||||
|
import { currentTheme } from '../stores/theme.js';
|
||||||
|
import { themes } from '../themes.js';
|
||||||
|
import FlagIcon from '../assets/flag.svg?raw';
|
||||||
|
|
||||||
export let spaces = [];
|
export let spaces = [];
|
||||||
export let authorization = '';
|
export let authorization = '';
|
||||||
|
|
@ -237,6 +240,14 @@
|
||||||
|
|
||||||
$: selectedType = spaceTypes.find(type => type.value === newSpaceType) || spaceTypes[0];
|
$: selectedType = spaceTypes.find(type => type.value === newSpaceType) || spaceTypes[0];
|
||||||
|
|
||||||
|
$: logoColor = (() => {
|
||||||
|
const theme = themes[$currentTheme];
|
||||||
|
if (theme && theme.colors['--red']) {
|
||||||
|
return theme.colors['--red'];
|
||||||
|
}
|
||||||
|
return '#ec3750';
|
||||||
|
})();
|
||||||
|
|
||||||
function togglePasswordVisibility() {
|
function togglePasswordVisibility() {
|
||||||
showPassword = !showPassword;
|
showPassword = !showPassword;
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +256,9 @@
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<header class="dashboard-header">
|
<header class="dashboard-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<img class="dashboard-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
|
<div class="dashboard-logo" style="color: {logoColor}">
|
||||||
|
{@html FlagIcon}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="dashboard-title">Hack Club Spaces</h1>
|
<h1 class="dashboard-title">Hack Club Spaces</h1>
|
||||||
<p class="welcome-text">Welcome, {username}!</p>
|
<p class="welcome-text">Welcome, {username}!</p>
|
||||||
|
|
|
||||||
126
client/src/lib/ThemeSwitcher.svelte
Normal file
126
client/src/lib/ThemeSwitcher.svelte
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<script>
|
||||||
|
import { currentTheme, setTheme } from '../stores/theme.js';
|
||||||
|
import { themes } from '../themes.js';
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTheme(themeName) {
|
||||||
|
setTheme(themeName);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (isOpen && !event.target.closest('.theme-switcher')) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={handleClickOutside} />
|
||||||
|
|
||||||
|
<div class="theme-switcher">
|
||||||
|
<button class="theme-toggle" on:click|stopPropagation={toggleMenu} aria-label="Switch theme">
|
||||||
|
theme: {themes[$currentTheme]?.name || 'light'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="theme-menu">
|
||||||
|
{#each Object.entries(themes) as [key, theme]}
|
||||||
|
<button
|
||||||
|
class="theme-option"
|
||||||
|
class:active={$currentTheme === key}
|
||||||
|
on:click={() => selectTheme(key)}
|
||||||
|
>
|
||||||
|
{theme.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.theme-switcher {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--white);
|
||||||
|
border: 2px solid var(--smoke);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--black);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-family: 'Phantom Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
border-color: var(--blue);
|
||||||
|
background: var(--snow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-menu {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 42px;
|
||||||
|
left: 0;
|
||||||
|
background: var(--white);
|
||||||
|
border: 2px solid var(--smoke);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--black);
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
font-family: 'Phantom Sans', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option:hover {
|
||||||
|
background: var(--snow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option.active {
|
||||||
|
background: var(--blue);
|
||||||
|
color: var(--white);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.theme-switcher {
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
client/src/stores/theme.js
Normal file
37
client/src/stores/theme.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { themes, defaultTheme } from '../themes.js';
|
||||||
|
|
||||||
|
const getInitialTheme = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('theme');
|
||||||
|
return saved && themes[saved] ? saved : defaultTheme;
|
||||||
|
}
|
||||||
|
return defaultTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const currentTheme = writable(getInitialTheme());
|
||||||
|
|
||||||
|
export const setTheme = (themeName) => {
|
||||||
|
if (themes[themeName]) {
|
||||||
|
currentTheme.set(themeName);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('theme', themeName);
|
||||||
|
applyTheme(themeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyTheme = (themeName) => {
|
||||||
|
const theme = themes[themeName];
|
||||||
|
if (theme && typeof document !== 'undefined') {
|
||||||
|
const root = document.documentElement;
|
||||||
|
Object.entries(theme.colors).forEach(([key, value]) => {
|
||||||
|
root.style.setProperty(key, value);
|
||||||
|
});
|
||||||
|
root.setAttribute('data-theme', themeName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
applyTheme(getInitialTheme());
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
.auth-container {
|
.auth-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #f9fafc;
|
background: var(--snow);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -18,13 +18,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form-panel {
|
.auth-form-panel {
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 64px;
|
padding: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-image-panel {
|
.auth-image-panel {
|
||||||
background: #f9fafc;
|
background: var(--snow);
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +37,15 @@
|
||||||
right: 50%;
|
right: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-container.verify-mode:not(.signup-mode) .auth-image-content {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container.verify-mode:not(.signup-mode) .auth-image-content::before {
|
||||||
|
background: linear-gradient(to bottom, rgba(51, 142, 218, 0.4) 0%, transparent 60%);
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-form-content {
|
.auth-form-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 440px;
|
max-width: 440px;
|
||||||
|
|
@ -84,13 +93,13 @@
|
||||||
left: 32px;
|
left: 32px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-caption a {
|
.image-caption a {
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
transition: opacity 0.125s ease-in-out;
|
transition: opacity 0.125s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +130,7 @@
|
||||||
.auth-title {
|
.auth-title {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
|
@ -129,7 +138,7 @@
|
||||||
|
|
||||||
.auth-subtitle {
|
.auth-subtitle {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
|
@ -139,11 +148,11 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-mode-link {
|
.auth-mode-link {
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.125s ease-in-out;
|
transition: color 0.125s ease-in-out;
|
||||||
|
|
@ -151,7 +160,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-mode-link:hover {
|
.auth-mode-link:hover {
|
||||||
color: #ff8c37;
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
|
|
@ -173,20 +182,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border: 2px solid #e0e6ed;
|
border: 2px solid var(--smoke);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -195,12 +214,12 @@
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #338eda;
|
border-color: var(--blue);
|
||||||
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input::placeholder {
|
.form-input::placeholder {
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
|
|
@ -209,8 +228,8 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
background: #ec3750;
|
background: var(--red);
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
|
|
@ -242,9 +261,9 @@
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid #ec3750;
|
border: 2px solid var(--red);
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
|
|
@ -269,9 +288,9 @@
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background: rgba(236, 55, 80, 0.1);
|
background: rgba(236, 55, 80, 0.1);
|
||||||
border: 2px solid #ec3750;
|
border: 2px solid var(--red);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,19 +298,64 @@
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background: rgba(51, 214, 166, 0.1);
|
background: rgba(51, 214, 166, 0.1);
|
||||||
border: 2px solid #33d6a6;
|
border: 2px solid var(--green);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #33d6a6;
|
color: var(--green);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-message {
|
.info-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verification-notice {
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: rgba(51, 142, 218, 0.05);
|
||||||
|
border: 2px solid rgba(51, 142, 218, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-text strong {
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-code-section {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
transition: opacity 0.4s ease-in-out, max-height 0.6s ease-in-out, margin-top 0.6s ease-in-out, padding-top 0.6s ease-in-out, border-color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-code-section.show {
|
||||||
|
max-height: 1000px;
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 32px;
|
||||||
|
border-top-color: var(--smoke);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.auth-container {
|
.auth-container {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
.dashboard {
|
.dashboard {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: #f9fafc;
|
background: var(--snow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 64px;
|
margin-bottom: 64px;
|
||||||
padding-bottom: 32px;
|
padding-bottom: 32px;
|
||||||
border-bottom: 2px solid #e0e6ed;
|
border-bottom: 2px solid var(--smoke);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
|
|
@ -24,25 +24,31 @@
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-logo svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-title {
|
.dashboard-title {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-text {
|
.welcome-text {
|
||||||
margin: 8px 0 0 0;
|
margin: 8px 0 0 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.signout-button {
|
.signout-button {
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid #ec3750;
|
border: 2px solid var(--red);
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
|
|
@ -73,8 +79,8 @@
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
padding: 14px 32px;
|
padding: 14px 32px;
|
||||||
background: #ec3750;
|
background: var(--red);
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
@ -103,9 +109,9 @@
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
padding: 14px 32px;
|
padding: 14px 32px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid #ec3750;
|
border: 2px solid var(--red);
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
|
|
@ -124,7 +130,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form {
|
.create-form {
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
@ -135,7 +141,7 @@
|
||||||
margin: 0 0 24px 0;
|
margin: 0 0 24px 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
|
|
@ -147,16 +153,16 @@
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border: 2px solid #e0e6ed;
|
border: 2px solid var(--smoke);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -165,10 +171,17 @@
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #338eda;
|
border-color: var(--blue);
|
||||||
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.password-input-wrapper {
|
.password-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -217,10 +230,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
padding-right: 48px;
|
padding-right: 48px;
|
||||||
border: 2px solid #e0e6ed;
|
border: 2px solid var(--smoke);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: Phantom Sans, sans-serif;
|
font-family: Phantom Sans, sans-serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -231,11 +244,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-trigger:hover {
|
.select-trigger:hover {
|
||||||
border-color: #338eda;
|
border-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-trigger.open {
|
.select-trigger.open {
|
||||||
border-color: #338eda;
|
border-color: var(--blue);
|
||||||
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +261,7 @@
|
||||||
height: 0;
|
height: 0;
|
||||||
border-left: 6px solid transparent;
|
border-left: 6px solid transparent;
|
||||||
border-right: 6px solid transparent;
|
border-right: 6px solid transparent;
|
||||||
border-top: 6px solid #3c4858;
|
border-top: 6px solid var(--slate);
|
||||||
transition: transform 0.125s ease-in-out;
|
transition: transform 0.125s ease-in-out;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
@ -262,8 +275,8 @@
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
border: 2px solid #338eda;
|
border: 2px solid var(--blue);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
@ -283,7 +296,7 @@
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.125s ease-in-out;
|
transition: background-color 0.125s ease-in-out;
|
||||||
border-bottom: 1px solid #e0e6ed;
|
border-bottom: 1px solid var(--smoke);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option:last-child {
|
.select-option:last-child {
|
||||||
|
|
@ -291,34 +304,34 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option:hover {
|
.select-option:hover {
|
||||||
background: #f9fafc;
|
background: var(--snow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option.selected {
|
.select-option.selected {
|
||||||
background: rgba(51, 142, 218, 0.1);
|
background: rgba(51, 142, 218, 0.1);
|
||||||
color: #338eda;
|
color: var(--blue);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-label {
|
.option-label {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-description {
|
.option-description {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background: rgba(236, 55, 80, 0.1);
|
background: rgba(236, 55, 80, 0.1);
|
||||||
border: 2px solid #ec3750;
|
border: 2px solid var(--red);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,14 +343,14 @@
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
margin: 0 0 24px 0;
|
margin: 0 0 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 64px 32px;
|
padding: 64px 32px;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,15 +361,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-card {
|
.space-card {
|
||||||
background: #ffffff;
|
background: var(--white);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e0e6ed;
|
border: 2px solid var(--smoke);
|
||||||
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
|
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-card:hover {
|
.space-card:hover {
|
||||||
border-color: #338eda;
|
border-color: var(--blue);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,7 +383,7 @@
|
||||||
.space-type {
|
.space-type {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2d3d;
|
color: var(--black);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
@ -380,23 +393,23 @@
|
||||||
border-radius: 99999px;
|
border-radius: 99999px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-running {
|
.status-running {
|
||||||
background: #33d6a6;
|
background: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-stopped {
|
.status-stopped {
|
||||||
background: #ec3750;
|
background: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-created {
|
.status-created {
|
||||||
background: #f1c40f;
|
background: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-unknown {
|
.status-unknown {
|
||||||
background: #8492a6;
|
background: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-info {
|
.space-info {
|
||||||
|
|
@ -406,21 +419,21 @@
|
||||||
|
|
||||||
.space-info p {
|
.space-info p {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: #8492a6;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-info strong {
|
.space-info strong {
|
||||||
color: #3c4858;
|
color: var(--slate);
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-info a {
|
.space-info a {
|
||||||
color: #338eda;
|
color: var(--blue);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.125s ease-in-out;
|
transition: color 0.125s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-info a:hover {
|
.space-info a:hover {
|
||||||
color: #ec3750;
|
color: var(--red);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,28 +473,33 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.start {
|
.action-btn.start {
|
||||||
background: #33d6a6;
|
background: var(--green);
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.stop {
|
.action-btn.stop {
|
||||||
background: #ec3750;
|
background: var(--red);
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.open {
|
.action-btn.open {
|
||||||
background: #338eda;
|
background: var(--blue);
|
||||||
color: #ffffff;
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.refresh {
|
.action-btn.refresh {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid #338eda;
|
border: 2px solid var(--blue);
|
||||||
color: #338eda;
|
color: var(--blue);
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-btn.delete {
|
||||||
|
background: var(--orange);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dashboard {
|
.dashboard {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
||||||
188
client/src/themes.js
Normal file
188
client/src/themes.js
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
export const themes = {
|
||||||
|
light: {
|
||||||
|
name: 'Light',
|
||||||
|
icon: 'Default',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ec3750',
|
||||||
|
'--orange': '#ff8c37',
|
||||||
|
'--yellow': '#f1c40f',
|
||||||
|
'--green': '#33d6a6',
|
||||||
|
'--cyan': '#5bc0de',
|
||||||
|
'--blue': '#338eda',
|
||||||
|
'--purple': '#a633d6',
|
||||||
|
'--muted': '#8492a6',
|
||||||
|
'--darker': '#121217',
|
||||||
|
'--dark': '#17171d',
|
||||||
|
'--darkless': '#252429',
|
||||||
|
'--black': '#1f2d3d',
|
||||||
|
'--steel': '#273444',
|
||||||
|
'--slate': '#3c4858',
|
||||||
|
'--smoke': '#e0e6ed',
|
||||||
|
'--snow': '#f9fafc',
|
||||||
|
'--white': '#ffffff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
name: 'Dark',
|
||||||
|
icon: 'Moon',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ff4060',
|
||||||
|
'--orange': '#ff9c47',
|
||||||
|
'--yellow': '#f9d423',
|
||||||
|
'--green': '#3ae6b6',
|
||||||
|
'--cyan': '#6bd0ee',
|
||||||
|
'--blue': '#5098ea',
|
||||||
|
'--purple': '#b643e6',
|
||||||
|
'--muted': '#9aa2b0',
|
||||||
|
'--darker': '#f9fafc',
|
||||||
|
'--dark': '#e0e6ed',
|
||||||
|
'--darkless': '#c0c6cd',
|
||||||
|
'--black': '#ffffff',
|
||||||
|
'--steel': '#e0e6ed',
|
||||||
|
'--slate': '#c0c6cd',
|
||||||
|
'--smoke': '#2a2a2f',
|
||||||
|
'--snow': '#1a1a1f',
|
||||||
|
'--white': '#0f0f14'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
darkPurple: {
|
||||||
|
name: 'Dark Purple',
|
||||||
|
icon: 'Sparkle',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ff6b9d',
|
||||||
|
'--orange': '#ffaa6b',
|
||||||
|
'--yellow': '#ffd966',
|
||||||
|
'--green': '#5de6c1',
|
||||||
|
'--cyan': '#7ddeff',
|
||||||
|
'--blue': '#7d9cff',
|
||||||
|
'--purple': '#c77dff',
|
||||||
|
'--muted': '#b4a5d6',
|
||||||
|
'--darker': '#f0e6ff',
|
||||||
|
'--dark': '#e0d1ff',
|
||||||
|
'--darkless': '#c9b3ff',
|
||||||
|
'--black': '#f5f0ff',
|
||||||
|
'--steel': '#d9ccff',
|
||||||
|
'--slate': '#c9b3ff',
|
||||||
|
'--smoke': '#2e1f47',
|
||||||
|
'--snow': '#1f0f3a',
|
||||||
|
'--white': '#15082b'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
darkGradient: {
|
||||||
|
name: 'Dark Gradient',
|
||||||
|
icon: 'Gradient',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ff4d6d',
|
||||||
|
'--orange': '#ff9057',
|
||||||
|
'--yellow': '#ffda77',
|
||||||
|
'--green': '#06ffa5',
|
||||||
|
'--cyan': '#00d9ff',
|
||||||
|
'--blue': '#4d8cff',
|
||||||
|
'--purple': '#b366ff',
|
||||||
|
'--muted': '#a0b0c0',
|
||||||
|
'--darker': '#e8f4f8',
|
||||||
|
'--dark': '#d0e4f0',
|
||||||
|
'--darkless': '#b0d4e8',
|
||||||
|
'--black': '#ffffff',
|
||||||
|
'--steel': '#c0d8e8',
|
||||||
|
'--slate': '#a0c0d8',
|
||||||
|
'--smoke': '#1a2a3a',
|
||||||
|
'--snow': '#0f1a2a',
|
||||||
|
'--white': '#0a0f1a'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lgbtq: {
|
||||||
|
name: 'Rainbow',
|
||||||
|
icon: 'Heart',
|
||||||
|
colors: {
|
||||||
|
'--red': '#E40303',
|
||||||
|
'--orange': '#FF8C00',
|
||||||
|
'--yellow': '#FFED00',
|
||||||
|
'--green': '#008026',
|
||||||
|
'--cyan': '#24408E',
|
||||||
|
'--blue': '#732982',
|
||||||
|
'--purple': '#732982',
|
||||||
|
'--muted': '#8b8b8b',
|
||||||
|
'--darker': '#ffffff',
|
||||||
|
'--dark': '#f5f5f5',
|
||||||
|
'--darkless': '#e8e8e8',
|
||||||
|
'--black': '#ffffff',
|
||||||
|
'--steel': '#e0e0e0',
|
||||||
|
'--slate': '#c0c0c0',
|
||||||
|
'--smoke': '#4a1942',
|
||||||
|
'--snow': '#2a0a26',
|
||||||
|
'--white': '#1a0616'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sunset: {
|
||||||
|
name: 'Sunset',
|
||||||
|
icon: 'Sunset',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ff6b6b',
|
||||||
|
'--orange': '#ffa06b',
|
||||||
|
'--yellow': '#ffe66d',
|
||||||
|
'--green': '#95e1d3',
|
||||||
|
'--cyan': '#6bcfff',
|
||||||
|
'--blue': '#a29bfe',
|
||||||
|
'--purple': '#fd79a8',
|
||||||
|
'--muted': '#b8a89d',
|
||||||
|
'--darker': '#2d3436',
|
||||||
|
'--dark': '#636e72',
|
||||||
|
'--darkless': '#95a5a6',
|
||||||
|
'--black': '#2d3436',
|
||||||
|
'--steel': '#636e72',
|
||||||
|
'--slate': '#95a5a6',
|
||||||
|
'--smoke': '#dfe6e9',
|
||||||
|
'--snow': '#f8f9fa',
|
||||||
|
'--white': '#ffffff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ocean: {
|
||||||
|
name: 'Ocean',
|
||||||
|
icon: 'Waves',
|
||||||
|
colors: {
|
||||||
|
'--red': '#ff6b9d',
|
||||||
|
'--orange': '#ff9f7f',
|
||||||
|
'--yellow': '#ffd97d',
|
||||||
|
'--green': '#69f0ae',
|
||||||
|
'--cyan': '#64d8cb',
|
||||||
|
'--blue': '#4d94ff',
|
||||||
|
'--purple': '#8c7ae6',
|
||||||
|
'--muted': '#7f8fa6',
|
||||||
|
'--darker': '#0f1c2e',
|
||||||
|
'--dark': '#1e3a5f',
|
||||||
|
'--darkless': '#2d5886',
|
||||||
|
'--black': '#e1f5fe',
|
||||||
|
'--steel': '#b3e5fc',
|
||||||
|
'--slate': '#81d4fa',
|
||||||
|
'--smoke': '#1e3a5f',
|
||||||
|
'--snow': '#0a1929',
|
||||||
|
'--white': '#021120'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forest: {
|
||||||
|
name: 'Forest',
|
||||||
|
icon: 'Tree',
|
||||||
|
colors: {
|
||||||
|
'--red': '#e74c3c',
|
||||||
|
'--orange': '#f39c12',
|
||||||
|
'--yellow': '#f1c40f',
|
||||||
|
'--green': '#27ae60',
|
||||||
|
'--cyan': '#16a085',
|
||||||
|
'--blue': '#3498db',
|
||||||
|
'--purple': '#9b59b6',
|
||||||
|
'--muted': '#95a5a6',
|
||||||
|
'--darker': '#0e2f1f',
|
||||||
|
'--dark': '#1a4731',
|
||||||
|
'--darkless': '#275f43',
|
||||||
|
'--black': '#ecf0f1',
|
||||||
|
'--steel': '#d5e8df',
|
||||||
|
'--slate': '#bdd4c7',
|
||||||
|
'--smoke': '#2c5f3f',
|
||||||
|
'--snow': '#1a4731',
|
||||||
|
'--white': '#0e2f1f'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTheme = 'light';
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // Binds to all network interfaces, accessible from other devices
|
host: '0.0.0.0',
|
||||||
port: 5173, // Default port, can be changed
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
3
package-lock.json
generated
3
package-lock.json
generated
|
|
@ -776,7 +776,6 @@
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
|
|
@ -818,7 +817,6 @@
|
||||||
"version": "8.2.1",
|
"version": "8.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "10.0.1"
|
"ip-address": "10.0.1"
|
||||||
},
|
},
|
||||||
|
|
@ -2719,7 +2717,6 @@
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue