diff --git a/webui/index.html b/webui/index.html
index 93946fc..d1fb86e 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -4,9 +4,6 @@
- {label &&
}
-
- {icon &&
{icon}}
-
+ {label &&
}
+
+ {icon && {icon}}
+
- {error &&
{error}
}
+ {error &&
{error}
}
);
-};
-
-export default Input;
+};
\ No newline at end of file
diff --git a/webui/src/common/components/Input/styles.sass b/webui/src/common/components/Input/styles.sass
index e446ebe..aaab3dd 100644
--- a/webui/src/common/components/Input/styles.sass
+++ b/webui/src/common/components/Input/styles.sass
@@ -1,57 +1,62 @@
.field
display: flex
flex-direction: column
- gap: .35rem
- font-size: .8rem
- font-weight: 500
+ gap: .5rem
+ font-size: .9rem
+ font-weight: 600
color: #374048
-.field__label
- letter-spacing: .5px
+.field-label
+ letter-spacing: .3px
+ margin-bottom: .2rem
-.field__control
+.field-control
position: relative
display: flex
align-items: center
background: #ffffff
- border: 1px solid #d0d7de
- border-radius: 6px
- padding: .55rem .7rem
- transition: border-color .15s ease, background .15s ease, box-shadow .15s ease
- &.has-icon .field__input
- padding-left: 1.6rem
+ border: 2px solid #e1e8f0
+ border-radius: 16px
+ padding: 1rem 1.2rem
+ transition: all .2s ease
+ &.has-icon .field-input
+ padding-left: 2.2rem
&.has-error
border-color: #d93025
- box-shadow: 0 0 0 1px #d93025
+ box-shadow: 0 0 0 4px rgba(217, 48, 37, 0.1)
&:focus-within
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
- left: .55rem
+ left: 1rem
top: 50%
transform: translateY(-50%)
display: inline-flex
- font-size: 1rem
+ font-size: 1.1rem
color: #6b7781
pointer-events: none
-.field__input
+.field-input
appearance: none
outline: none
background: transparent
border: 0
color: #1f2429
font: inherit
+ font-size: 1rem
+ font-weight: 500
width: 100%
- line-height: 1.2
+ line-height: 1.3
&::placeholder
color: #a0abb4
+ font-weight: 400
&:focus
outline: none
-.field__error
+.field-error
font-size: .65rem
font-weight: 600
color: #d93025
diff --git a/webui/src/common/components/ProfileMenu/ProfileMenu.jsx b/webui/src/common/components/ProfileMenu/ProfileMenu.jsx
new file mode 100644
index 0000000..71b94d4
--- /dev/null
+++ b/webui/src/common/components/ProfileMenu/ProfileMenu.jsx
@@ -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 (
+
+
+
+ {isOpen && (
+
+
+
+
+
+
+
Admin User
+
Administrator
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/webui/src/common/components/ProfileMenu/index.js b/webui/src/common/components/ProfileMenu/index.js
new file mode 100644
index 0000000..f5ca9f3
--- /dev/null
+++ b/webui/src/common/components/ProfileMenu/index.js
@@ -0,0 +1 @@
+export { ProfileMenu as default } from './ProfileMenu';
\ No newline at end of file
diff --git a/webui/src/common/components/ProfileMenu/styles.sass b/webui/src/common/components/ProfileMenu/styles.sass
new file mode 100644
index 0000000..a6f6869
--- /dev/null
+++ b/webui/src/common/components/ProfileMenu/styles.sass
@@ -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)
\ No newline at end of file
diff --git a/webui/src/common/components/Sidebar/Sidebar.jsx b/webui/src/common/components/Sidebar/Sidebar.jsx
new file mode 100644
index 0000000..3f77d81
--- /dev/null
+++ b/webui/src/common/components/Sidebar/Sidebar.jsx
@@ -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:
},
+ { to: '/servers', label: 'Servers', icon:
},
+ { to: '/settings', label: 'Settings', icon:
},
+];
+
+const adminNavItems = [
+ { to: '/admin/users', label: 'User Management', icon:
},
+];
+
+export const Sidebar = () => {
+ const { user } = useContext(UserContext);
+ const isAdmin = user?.role === 'admin' || user?.is_admin === true;
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/webui/src/common/components/Sidebar/index.js b/webui/src/common/components/Sidebar/index.js
new file mode 100644
index 0000000..86aee2c
--- /dev/null
+++ b/webui/src/common/components/Sidebar/index.js
@@ -0,0 +1 @@
+export { Sidebar as default } from './Sidebar';
\ No newline at end of file
diff --git a/webui/src/common/components/Sidebar/styles.sass b/webui/src/common/components/Sidebar/styles.sass
new file mode 100644
index 0000000..ea39d6c
--- /dev/null
+++ b/webui/src/common/components/Sidebar/styles.sass
@@ -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
\ No newline at end of file
diff --git a/webui/src/common/layouts/Root.jsx b/webui/src/common/layouts/Root.jsx
index 60f068b..4f403bc 100644
--- a/webui/src/common/layouts/Root.jsx
+++ b/webui/src/common/layouts/Root.jsx
@@ -1,30 +1,17 @@
-import React, { useContext } from 'react';
-import { NavLink, Outlet } from 'react-router-dom';
-import { HouseIcon, GearSixIcon, SquaresFourIcon } from '@phosphor-icons/react';
-
-const navItems = [
- { to: '/dashboard', label: 'Dashboard', icon:
},
- { to: '/servers', label: 'Servers', icon:
},
- { to: '/settings', label: 'Settings', icon:
},
-];
+import React from 'react';
+import { Outlet } from 'react-router-dom';
+import Sidebar from '@/common/components/Sidebar';
+import ProfileMenu from '@/common/components/ProfileMenu';
const Root = () => {
return (
-
+
diff --git a/webui/src/common/styles/main.sass b/webui/src/common/styles/main.sass
index 1fa8afe..3180aa7 100644
--- a/webui/src/common/styles/main.sass
+++ b/webui/src/common/styles/main.sass
@@ -43,48 +43,6 @@ a
display: flex
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
flex: 1
display: flex
@@ -108,50 +66,4 @@ a
padding: 1.4rem 1.3rem 2rem
display: flex
flex-direction: column
- 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
\ No newline at end of file
+ gap: 1.2rem
\ No newline at end of file
diff --git a/webui/src/pages/Login/Login.jsx b/webui/src/pages/Login/Login.jsx
index 542c2af..b9f4233 100644
--- a/webui/src/pages/Login/Login.jsx
+++ b/webui/src/pages/Login/Login.jsx
@@ -13,6 +13,7 @@ export const Login = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [mode, setMode] = useState(isSetupCompleted ? 'login' : 'setup');
+ const [isExiting, setIsExiting] = useState(false);
React.useEffect(()=>{
setMode(isSetupCompleted ? 'login' : 'setup');
@@ -25,28 +26,48 @@ export const Login = () => {
try {
if (mode === 'login') {
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
await request('/setup/init','POST',{username, password});
const data = await request('/auth/login','POST',{username, password});
- updateSessionToken(data.token);
+ setIsExiting(true);
+ setTimeout(() => {
+ updateSessionToken(data.token);
+ }, 300);
}
} catch (err) {
setError(err.error || err.message || 'Error');
- } finally {
setLoading(false);
}
};
return (
-
-
-
{mode === 'login' ? 'Sign in' : 'Initial Setup'}
+
+
+
+
{mode === 'login' ? 'Welcome Back' : 'Initial Setup'}
+
+ {mode === 'login'
+ ? 'Sign in to your account to continue'
+ : 'Create your admin account to get started'
+ }
+
+
diff --git a/webui/src/pages/Login/styles.sass b/webui/src/pages/Login/styles.sass
index 6f090d7..a66d538 100644
--- a/webui/src/pages/Login/styles.sass
+++ b/webui/src/pages/Login/styles.sass
@@ -1,11 +1,130 @@
-.auth-box
- box-shadow: 0 0 0 1px rgba(255,255,255,.02), 0 4px 16px -6px rgba(0,0,0,.6)
- animation: fadeIn .3s ease
+.auth-wrapper
+ min-height: 100vh
+ 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
opacity: 0
- transform: translateY(4px)
+ backdrop-filter: blur(0px)
to
opacity: 1
- transform: translateY(0)
\ No newline at end of file
+ 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)
\ No newline at end of file