Add UI components

This commit is contained in:
Mathias Wagner
2025-09-09 13:07:35 +02:00
parent 12f9eebfad
commit e39a583e95
16 changed files with 580 additions and 198 deletions

View File

@@ -4,9 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arkendro</title> <title>Arkendro</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -20,6 +20,7 @@ const App = () => {
{ path: "/dashboard", element: <Placeholder title="Dashboard" /> }, { path: "/dashboard", element: <Placeholder title="Dashboard" /> },
{ path: "/servers", element: <Placeholder title="Servers" /> }, { path: "/servers", element: <Placeholder title="Servers" /> },
{ path: "/settings", element: <Placeholder title="Settings" /> }, { path: "/settings", element: <Placeholder title="Settings" /> },
{ path: "/admin/users", element: <Placeholder title="User Management" /> },
], ],
}, },
]); ]);

View File

@@ -2,16 +2,6 @@ import React from "react";
import cn from "classnames"; import cn from "classnames";
import "./styles.sass"; import "./styles.sass";
/*
Props:
- variant: primary | subtle | danger
- size: sm | md | lg
- full: boolean (full width)
- icon: ReactNode (left icon)
- iconRight: ReactNode (right icon)
- loading: boolean
*/
export const Button = ({ export const Button = ({
as: Component = "button", as: Component = "button",
variant = "primary", variant = "primary",
@@ -39,12 +29,10 @@ export const Button = ({
disabled={isDisabled} disabled={isDisabled}
{...rest} {...rest}
> >
{loading && <span className="btn__spinner" aria-hidden />} {loading && <span className="btn-spinner" aria-hidden />}
{icon && <span className="btn__icon btn__icon--left">{icon}</span>} {icon && <span className="btn-icon btn-icon--left">{icon}</span>}
<span className="btn__label">{children}</span> <span className="btn-label">{children}</span>
{iconRight && <span className="btn__icon btn__icon--right">{iconRight}</span>} {iconRight && <span className="btn-icon btn-icon--right">{iconRight}</span>}
</Component> </Component>
); );
}; };
export default Button;

View File

@@ -2,8 +2,8 @@
--c-bg: #ffffff --c-bg: #ffffff
--c-bg-hover: #f2f5f8 --c-bg-hover: #f2f5f8
--c-bg-active: #e6ebf0 --c-bg-active: #e6ebf0
--c-border: #d0d7de --c-border: #dfe3e8
--c-border-hover: #c2cbd3 --c-border-hover: #c7ced6
--c-text: #1f2429 --c-text: #1f2429
--c-accent: #0f62fe --c-accent: #0f62fe
--c-danger: #d93025 --c-danger: #d93025
@@ -11,16 +11,16 @@
display: inline-flex display: inline-flex
align-items: center align-items: center
justify-content: center justify-content: center
gap: .5rem gap: .6rem
font-family: inherit font-family: inherit
font-weight: 500 font-weight: 600
line-height: 1.2 line-height: 1.2
cursor: pointer cursor: pointer
border: 1px solid var(--c-border) border: 1px solid var(--c-border)
background: var(--c-bg) background: var(--c-bg)
color: var(--c-text) color: var(--c-text)
border-radius: 6px border-radius: 12px
transition: background .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease transition: all .2s ease
user-select: none user-select: none
text-decoration: none text-decoration: none
&:hover:not(:disabled) &:hover:not(:disabled)
@@ -28,6 +28,7 @@
border-color: var(--c-border-hover) border-color: var(--c-border-hover)
&:active:not(:disabled) &:active:not(:disabled)
background: var(--c-bg-active) background: var(--c-bg-active)
transform: translateY(1px)
&:focus-visible &:focus-visible
outline: 2px solid var(--c-accent) outline: 2px solid var(--c-accent)
outline-offset: 2px outline-offset: 2px
@@ -37,33 +38,39 @@
&.btn--full &.btn--full
width: 100% width: 100%
&.btn--sm &.btn--sm
font-size: .75rem
padding: .4rem .7rem
&.btn--md
font-size: .85rem font-size: .85rem
padding: .6rem 1rem padding: .7rem 1rem
&.btn--md
font-size: .95rem
padding: .85rem 1.25rem
&.btn--lg &.btn--lg
font-size: 1rem font-size: 1.05rem
padding: .8rem 1.2rem padding: 1rem 1.5rem
&.btn--primary &.btn--primary
--c-bg: #0f62fe --c-bg: #1f2429
--c-bg-hover: #0d55dd --c-bg-hover: #374048
--c-bg-active: #0b47b8 --c-bg-active: #2a3038
--c-border: #0f62fe --c-border: #1f2429
--c-text: #ffffff --c-text: #ffffff
background: var(--c-bg)
border-color: var(--c-border)
&:hover:not(:disabled)
background: var(--c-bg-hover)
&.btn--subtle &.btn--subtle
--c-bg: #f3f6f9 --c-bg: #f0f3f6
--c-bg-hover: #e8edf2 --c-bg-hover: #e6ebf0
--c-bg-active: #dfe5eb --c-bg-active: #dfe3e8
--c-border: #e1e6eb --c-border: #dfe3e8
&.btn--danger &.btn--danger
--c-bg: #d93025 --c-bg: #d93025
--c-bg-hover: #c22b21 --c-bg-hover: #c22b21
--c-bg-active: #a9241b --c-bg-active: #a9241b
--c-border: #d93025 --c-border: #d93025
--c-text: #ffffff --c-text: #ffffff
background: var(--c-bg)
border-color: var(--c-border)
.btn__icon .btn-icon
display: inline-flex display: inline-flex
align-items: center align-items: center
&--left &--left
@@ -71,7 +78,7 @@
&--right &--right
margin-left: .25rem margin-left: .25rem
.btn__spinner .btn-spinner
width: 14px width: 14px
height: 14px height: 14px
border: 2px solid rgba(0,0,0,.15) border: 2px solid rgba(0,0,0,.15)

View File

@@ -1,8 +1,6 @@
import React from "react"; import React from "react";
import cn from "classnames"; import cn from "classnames";
import "./styles.sass"; import "./styles.sass";
/* Minimal input wrapper with label, error text, and optional icon */
export const Input = ({ export const Input = ({
label, label,
error, error,
@@ -14,14 +12,12 @@ export const Input = ({
}) => { }) => {
return ( return (
<div className={cn("field", containerClassName)}> <div className={cn("field", containerClassName)}>
{label && <label className="field__label">{label}</label>} {label && <label className="field-label">{label}</label>}
<div className={cn("field__control", error && "has-error", icon && "has-icon", className)}> <div className={cn("field-control", error && "has-error", icon && "has-icon", className)}>
{icon && <span className="field__icon">{icon}</span>} {icon && <span className="field-icon">{icon}</span>}
<input type={type} className="field__input" {...rest} /> <input type={type} className="field-input" {...rest} />
</div> </div>
{error && <div className="field__error">{error}</div>} {error && <div className="field-error">{error}</div>}
</div> </div>
); );
}; };
export default Input;

View File

@@ -1,57 +1,62 @@
.field .field
display: flex display: flex
flex-direction: column flex-direction: column
gap: .35rem gap: .5rem
font-size: .8rem font-size: .9rem
font-weight: 500 font-weight: 600
color: #374048 color: #374048
.field__label .field-label
letter-spacing: .5px letter-spacing: .3px
margin-bottom: .2rem
.field__control .field-control
position: relative position: relative
display: flex display: flex
align-items: center align-items: center
background: #ffffff background: #ffffff
border: 1px solid #d0d7de border: 2px solid #e1e8f0
border-radius: 6px border-radius: 16px
padding: .55rem .7rem padding: 1rem 1.2rem
transition: border-color .15s ease, background .15s ease, box-shadow .15s ease transition: all .2s ease
&.has-icon .field__input &.has-icon .field-input
padding-left: 1.6rem padding-left: 2.2rem
&.has-error &.has-error
border-color: #d93025 border-color: #d93025
box-shadow: 0 0 0 1px #d93025 box-shadow: 0 0 0 4px rgba(217, 48, 37, 0.1)
&:focus-within &:focus-within
border-color: #0f62fe border-color: #0f62fe
box-shadow: 0 0 0 1px #0f62fe20 box-shadow: 0 0 0 4px rgba(15, 98, 254, 0.1)
transform: translateY(-1px)
.field__icon .field-icon
position: absolute position: absolute
left: .55rem left: 1rem
top: 50% top: 50%
transform: translateY(-50%) transform: translateY(-50%)
display: inline-flex display: inline-flex
font-size: 1rem font-size: 1.1rem
color: #6b7781 color: #6b7781
pointer-events: none pointer-events: none
.field__input .field-input
appearance: none appearance: none
outline: none outline: none
background: transparent background: transparent
border: 0 border: 0
color: #1f2429 color: #1f2429
font: inherit font: inherit
font-size: 1rem
font-weight: 500
width: 100% width: 100%
line-height: 1.2 line-height: 1.3
&::placeholder &::placeholder
color: #a0abb4 color: #a0abb4
font-weight: 400
&:focus &:focus
outline: none outline: none
.field__error .field-error
font-size: .65rem font-size: .65rem
font-weight: 600 font-weight: 600
color: #d93025 color: #d93025

View File

@@ -0,0 +1,95 @@
import React, {useState, useRef, useEffect, useContext} from 'react';
import {UserCircleIcon, SignOutIcon, CaretDownIcon} from '@phosphor-icons/react';
import {UserContext} from '@/common/contexts/UserContext.jsx';
import './styles.sass';
export const ProfileMenu = () => {
const {logout} = useContext(UserContext);
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef(null);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Close menu on escape key
useEffect(() => {
const handleEscapeKey = (event) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('keydown', handleEscapeKey);
};
}
}, [isOpen]);
const handleLogout = () => {
setIsOpen(false);
logout();
};
const toggleMenu = () => {
setIsOpen(!isOpen);
};
return (
<div className="profile-menu" ref={menuRef}>
<button
className={`profile-menu-trigger ${isOpen ? 'active' : ''}`}
onClick={toggleMenu}
aria-label="User menu"
aria-expanded={isOpen}
>
<div className="profile-menu-avatar">
<UserCircleIcon size={16} weight="fill"/>
</div>
<span className="profile-menu-name">Admin</span>
<CaretDownIcon
size={14}
className={`profile-menu-caret ${isOpen ? 'rotated' : ''}`}
/>
</button>
{isOpen && (
<div className="profile-menu-dropdown">
<div className="profile-menu-header">
<div className="profile-menu-avatar profile-menu-avatar--large">
<UserCircleIcon size={20} weight="fill"/>
</div>
<div className="profile-menu-info">
<div className="profile-menu-name-large">Admin User</div>
<div className="profile-menu-role">Administrator</div>
</div>
</div>
<div className="profile-menu-divider"></div>
<div className="profile-menu-actions">
<button
className="profile-menu-item"
onClick={handleLogout}
>
<SignOutIcon size={16}/>
<span>Sign Out</span>
</button>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1 @@
export { ProfileMenu as default } from './ProfileMenu';

View File

@@ -0,0 +1,123 @@
.profile-menu
position: relative
display: inline-block
.profile-menu-trigger
display: flex
align-items: center
gap: .5rem
padding: .5rem .75rem
background: transparent
border: 1px solid var(--border)
border-radius: 10px
color: var(--text)
font-size: .85rem
font-weight: 500
cursor: pointer
transition: all .2s ease
&:hover
background: var(--bg-elev)
border-color: var(--border-strong)
&.active
background: var(--bg-elev)
border-color: var(--border-strong)
.profile-menu-caret
transform: rotate(180deg)
.profile-menu-avatar
width: 28px
height: 28px
background: rgba(15, 98, 254, 0.15)
border: 1px solid rgba(15, 98, 254, 0.25)
border-radius: 50%
display: flex
align-items: center
justify-content: center
color: var(--accent)
flex-shrink: 0
backdrop-filter: blur(8px)
&--large
width: 36px
height: 36px
background: rgba(15, 98, 254, 0.2)
border: 1px solid rgba(15, 98, 254, 0.3)
.profile-menu-name
font-weight: 600
white-space: nowrap
.profile-menu-caret
transition: transform .2s ease
color: var(--text-dim)
&.rotated
transform: rotate(180deg)
.profile-menu-dropdown
position: absolute
top: calc(100% + .5rem)
right: 0
min-width: 200px
background: var(--bg-alt)
border: 1px solid var(--border)
border-radius: 12px
box-shadow: 0 8px 32px -8px rgba(31,36,41,.15), 0 4px 16px -4px rgba(31,36,41,.1)
z-index: 1000
animation: dropdownFadeIn .2s ease-out
.profile-menu-header
padding: 1rem
display: flex
align-items: center
gap: .75rem
.profile-menu-info
flex: 1
min-width: 0
.profile-menu-name-large
font-size: .9rem
font-weight: 600
color: var(--text)
margin-bottom: .1rem
.profile-menu-role
font-size: .75rem
color: var(--text-dim)
font-weight: 500
.profile-menu-divider
height: 1px
background: var(--border)
margin: 0 .5rem
.profile-menu-actions
padding: .5rem
.profile-menu-item
display: flex
align-items: center
gap: .75rem
width: 100%
padding: .75rem
background: transparent
border: none
border-radius: 8px
color: var(--text)
font-size: .85rem
font-weight: 500
cursor: pointer
transition: all .15s ease
text-align: left
&:hover
background: var(--bg-elev)
color: var(--danger)
svg
color: var(--danger)
@keyframes dropdownFadeIn
from
opacity: 0
transform: translateY(-4px) scale(0.95)
to
opacity: 1
transform: translateY(0) scale(1)

View File

@@ -0,0 +1,60 @@
import React, { useContext } from 'react';
import { NavLink } from 'react-router-dom';
import { HouseIcon, GearSixIcon, SquaresFourIcon, CubeIcon, UsersIcon } from '@phosphor-icons/react';
import { UserContext } from '@/common/contexts/UserContext.jsx';
import './styles.sass';
const navItems = [
{ to: '/dashboard', label: 'Dashboard', icon: <HouseIcon weight="duotone" /> },
{ to: '/servers', label: 'Servers', icon: <SquaresFourIcon weight="duotone" /> },
{ to: '/settings', label: 'Settings', icon: <GearSixIcon weight="duotone" /> },
];
const adminNavItems = [
{ to: '/admin/users', label: 'User Management', icon: <UsersIcon weight="duotone" /> },
];
export const Sidebar = () => {
const { user } = useContext(UserContext);
const isAdmin = user?.role === 'admin' || user?.is_admin === true;
return (
<aside className="sidebar">
<div className="sidebar-brand">
<CubeIcon size={24} weight="duotone" />
<span>Arkendro</span>
</div>
<nav className="sidebar-nav">
<div className="nav-section">
{navItems.map(item => (
<NavLink
key={item.to}
to={item.to}
className={({isActive}) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="nav-item-icon">{item.icon}</span>
<span>{item.label}</span>
</NavLink>
))}
</div>
{isAdmin && (
<div className="nav-section">
<div className="nav-section-title">Admin</div>
{adminNavItems.map(item => (
<NavLink
key={item.to}
to={item.to}
className={({isActive}) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="nav-item-icon">{item.icon}</span>
<span>{item.label}</span>
</NavLink>
))}
</div>
)}
</nav>
</aside>
);
};

View File

@@ -0,0 +1 @@
export { Sidebar as default } from './Sidebar';

View File

@@ -0,0 +1,69 @@
.sidebar
width: var(--sidebar-width)
background: var(--bg-alt)
border-right: 1px solid var(--border)
display: flex
flex-direction: column
padding: 1.5rem 1.2rem
gap: 1.8rem
.sidebar-brand
font-size: 1.1rem
font-weight: 700
letter-spacing: .3px
display: flex
align-items: center
gap: .75rem
color: var(--text)
padding-bottom: .5rem
svg
color: var(--accent)
flex-shrink: 0
.sidebar-nav
display: flex
flex-direction: column
gap: 1.2rem
.nav-section
display: flex
flex-direction: column
gap: .4rem
.nav-section-title
font-size: .75rem
font-weight: 700
text-transform: uppercase
letter-spacing: .8px
color: var(--text-dim)
margin-bottom: .3rem
padding: 0 .9rem
.nav-item
display: flex
align-items: center
gap: .75rem
padding: .75rem .9rem
color: var(--text-dim)
font-size: .85rem
font-weight: 600
letter-spacing: .3px
border-radius: 10px
transition: all .2s ease
cursor: pointer
text-decoration: none
&:hover
background: var(--bg-elev)
color: var(--text)
transform: translateX(2px)
&.active
background: var(--text)
color: #ffffff
font-weight: 700
box-shadow: 0 2px 8px -2px rgba(31, 36, 41, 0.2)
.nav-item-icon
font-size: 1.2rem
display: inline-flex
color: inherit
min-width: 1.2rem

View File

@@ -1,30 +1,17 @@
import React, { useContext } from 'react'; import React from 'react';
import { NavLink, Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { HouseIcon, GearSixIcon, SquaresFourIcon } from '@phosphor-icons/react'; import Sidebar from '@/common/components/Sidebar';
import ProfileMenu from '@/common/components/ProfileMenu';
const navItems = [
{ to: '/dashboard', label: 'Dashboard', icon: <HouseIcon weight="duotone" /> },
{ to: '/servers', label: 'Servers', icon: <SquaresFourIcon weight="duotone" /> },
{ to: '/settings', label: 'Settings', icon: <GearSixIcon weight="duotone" /> },
];
const Root = () => { const Root = () => {
return ( return (
<div className="layout"> <div className="layout">
<aside className="sidebar"> <Sidebar />
<div className="sidebar__brand">Arkendro</div>
<nav className="sidebar__nav">
{navItems.map(item => (
<NavLink key={item.to} to={item.to} className={({isActive}) => `nav-item ${isActive ? 'active' : ''}`}>
<span className="nav-item__icon">{item.icon}</span>
<span>{item.label}</span>
</NavLink>
))}
</nav>
</aside>
<div className="main"> <div className="main">
<header className="topbar"> <header className="topbar">
<h3>Test</h3> <h3>Dashboard</h3>
<div className="grow"></div>
<ProfileMenu />
</header> </header>
<Outlet /> <Outlet />
</div> </div>

View File

@@ -43,48 +43,6 @@ a
display: flex display: flex
min-height: 100vh min-height: 100vh
.sidebar
width: var(--sidebar-width)
background: var(--bg-alt)
border-right: 1px solid var(--border)
display: flex
flex-direction: column
padding: 1rem .9rem
gap: 1.2rem
.sidebar__brand
font-size: .95rem
font-weight: 600
letter-spacing: .5px
display: flex
align-items: center
gap: .55rem
.sidebar__nav
display: flex
flex-direction: column
gap: .2rem
.nav-item
display: flex
align-items: center
gap: .65rem
padding: .6rem .7rem
color: var(--text-dim)
font-size: .75rem
font-weight: 500
letter-spacing: .5px
border-radius: var(--radius-sm)
transition: background .15s ease, color .15s ease
&.active, &:hover
background: var(--bg-elev)
color: var(--text)
.nav-item__icon
font-size: 1.1rem
display: inline-flex
color: inherit
.main .main
flex: 1 flex: 1
display: flex display: flex
@@ -109,49 +67,3 @@ a
display: flex display: flex
flex-direction: column flex-direction: column
gap: 1.2rem gap: 1.2rem
/* Auth page */
.auth-wrapper
min-height: 100vh
display: flex
align-items: center
justify-content: center
padding: 2rem 1rem
.auth-box
width: 100%
max-width: 380px
background: var(--bg-alt)
border: 1px solid var(--border)
border-radius: var(--radius-lg)
padding: 1.8rem 1.6rem 1.7rem
display: flex
flex-direction: column
gap: 1.2rem
box-shadow: 0 4px 14px -4px rgba(31,36,41,.08), 0 2px 4px -1px rgba(31,36,41,.06)
.auth-title
font-size: 1.1rem
font-weight: 600
letter-spacing: .5px
.form
display: flex
flex-direction: column
gap: .95rem
.sep
height: 1px
background: var(--border)
margin: .25rem 0
.text-center
text-align: center
.small
font-size: .65rem
color: var(--text-dim)
.inline
display: inline

View File

@@ -13,6 +13,7 @@ export const Login = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [mode, setMode] = useState(isSetupCompleted ? 'login' : 'setup'); const [mode, setMode] = useState(isSetupCompleted ? 'login' : 'setup');
const [isExiting, setIsExiting] = useState(false);
React.useEffect(()=>{ React.useEffect(()=>{
setMode(isSetupCompleted ? 'login' : 'setup'); setMode(isSetupCompleted ? 'login' : 'setup');
@@ -25,28 +26,48 @@ export const Login = () => {
try { try {
if (mode === 'login') { if (mode === 'login') {
const data = await request('/auth/login','POST',{username, password}); const data = await request('/auth/login','POST',{username, password});
updateSessionToken(data.token); // Trigger fade-out animation before updating token
setIsExiting(true);
setTimeout(() => {
updateSessionToken(data.token);
}, 300); // Match the fade-out animation duration
} else { // setup } else { // setup
await request('/setup/init','POST',{username, password}); await request('/setup/init','POST',{username, password});
const data = await request('/auth/login','POST',{username, password}); const data = await request('/auth/login','POST',{username, password});
updateSessionToken(data.token); setIsExiting(true);
setTimeout(() => {
updateSessionToken(data.token);
}, 300);
} }
} catch (err) { } catch (err) {
setError(err.error || err.message || 'Error'); setError(err.error || err.message || 'Error');
} finally {
setLoading(false); setLoading(false);
} }
}; };
return ( return (
<div className="auth-wrapper"> <div className={`auth-wrapper ${isExiting ? 'fade-out' : ''}`}>
<div className="auth-box"> <div className={`auth-box ${isExiting ? 'fade-out' : ''}`}>
<div className="auth-title">{mode === 'login' ? 'Sign in' : 'Initial Setup'}</div> <div className="auth-header">
<div className="auth-title">{mode === 'login' ? 'Welcome Back' : 'Initial Setup'}</div>
<div className="auth-subtitle">
{mode === 'login'
? 'Sign in to your account to continue'
: 'Create your admin account to get started'
}
</div>
</div>
<form className="form" onSubmit={submit}> <form className="form" onSubmit={submit}>
<Input label="Username" placeholder="your name" value={username} onChange={e=>setUsername(e.target.value)} icon={<UserIcon size={16} />} autoFocus required /> <div className="form-field form-field--1">
<Input type="password" label="Password" placeholder="••••••••" value={password} onChange={e=>setPassword(e.target.value)} icon={<LockIcon size={16} />} required /> <Input label="Username" placeholder="your name" value={username} onChange={e=>setUsername(e.target.value)} icon={<UserIcon size={16} />} autoFocus required />
{error && <div style={{color:'#dc3545', fontSize:'.65rem', fontWeight:600}}>{error}</div>} </div>
<Button type="submit" loading={loading} variant="primary" full>{mode === 'login' ? 'Login' : 'Create Admin & Continue'}</Button> <div className="form-field form-field--2">
<Input type="password" label="Password" placeholder="••••••••" value={password} onChange={e=>setPassword(e.target.value)} icon={<LockIcon size={16} />} required />
</div>
{error && <div className="auth-error form-field form-field--3">{error}</div>}
<div className="form-field form-field--4">
<Button type="submit" loading={loading} variant="primary" full>{mode === 'login' ? 'Login' : 'Create Admin & Continue'}</Button>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,130 @@
.auth-box .auth-wrapper
box-shadow: 0 0 0 1px rgba(255,255,255,.02), 0 4px 16px -6px rgba(0,0,0,.6) min-height: 100vh
animation: fadeIn .3s ease display: flex
align-items: center
justify-content: center
padding: 2rem 1rem
background: var(--bg)
animation: fadeInWrapper .4s ease-out
@keyframes fadeIn .auth-box
width: 100%
max-width: 480px
background: var(--bg-alt)
border: 2px solid var(--border)
border-radius: 24px
padding: 3rem 2.5rem 2.8rem
display: flex
flex-direction: column
gap: 1.8rem
box-shadow: 0 8px 32px -8px rgba(31,36,41,.12), 0 4px 16px -4px rgba(31,36,41,.08)
position: relative
overflow: hidden
animation: fadeInUp .6s ease-out
.auth-title
font-size: 1.75rem
font-weight: 700
letter-spacing: -0.5px
text-align: center
color: var(--text)
margin-bottom: 0.5rem
.auth-header
text-align: center
margin-bottom: 1rem
.auth-subtitle
font-size: 1rem
font-weight: 400
color: var(--text-dim)
text-align: center
line-height: 1.4
.auth-error
background: rgba(217, 48, 37, 0.1)
border: 1px solid rgba(217, 48, 37, 0.2)
border-radius: 12px
padding: 0.75rem 1rem
color: #d93025
font-size: 0.9rem
font-weight: 600
text-align: center
.form
display: flex
flex-direction: column
gap: 1.5rem
@keyframes fadeInWrapper
from from
opacity: 0 opacity: 0
transform: translateY(4px) backdrop-filter: blur(0px)
to to
opacity: 1 opacity: 1
transform: translateY(0) backdrop-filter: blur(2px)
@keyframes fadeInUp
from
opacity: 0
transform: translateY(40px) scale(0.9)
filter: blur(4px)
50%
opacity: 0.8
transform: translateY(20px) scale(0.95)
filter: blur(2px)
to
opacity: 1
transform: translateY(0) scale(1)
filter: blur(0px)
// Fade out animations for when dialog closes
.auth-wrapper.fade-out
animation: fadeOutWrapper .3s ease-in forwards
.auth-box.fade-out
animation: fadeOutDown .3s ease-in forwards
@keyframes fadeOutWrapper
from
opacity: 1
backdrop-filter: blur(2px)
to
opacity: 0
backdrop-filter: blur(0px)
@keyframes fadeOutDown
from
opacity: 1
transform: translateY(0) scale(1)
filter: blur(0px)
to
opacity: 0
transform: translateY(20px) scale(0.95)
filter: blur(2px)
// Staggered animations for form elements
.form-field
animation: slideInLeft .5s ease-out both
.form-field--1
animation-delay: .1s
.form-field--2
animation-delay: .2s
.form-field--3
animation-delay: .3s
.form-field--4
animation-delay: .4s
@keyframes slideInLeft
from
opacity: 0
transform: translateX(-20px)
filter: blur(2px)
to
opacity: 1
transform: translateX(0)
filter: blur(0px)