feat: complete UI redesign with dark mode, sidebar navigation, and modern design system

Implemented comprehensive UI overhaul with three-layer architecture:

Layer 1 - Theme System:
- CSS variables for light/dark themes (theme.css)
- Theme composable with light/dark/auto mode (useTheme.ts)
- Sidebar state management composable (useSidebar.ts)
- Refactored main.css to use CSS variables throughout

Layer 2 - Core Components:
- AppSidebar with collapsible navigation (desktop) and overlay (mobile)
- CollapsibleSection reusable component for expandable cards
- Restructured App.vue with new sidebar layout
- Integrated Lucide icons library (lucide-vue-next)

Layer 3 - Views & Components:
- Updated all 14 views with CSS variables and responsive design
- Replaced inline SVG with Lucide icon components
- Added collapsible sections to Dashboard, Admin pages, UserProfile
- Updated 3 shared components (BookingForm, SpaceCalendar, AttachmentsList)

Features:
- Dark/light/auto theme with persistent preference
- Collapsible sidebar (icons-only on desktop, overlay on mobile)
- Consistent color palette using CSS variables
- Full responsive design across all pages
- Modern minimalist aesthetic with Indigo accent color

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-11 21:27:05 +00:00
parent 9c2846cf00
commit 0bf3e6a7e2
28 changed files with 1960 additions and 1641 deletions

View File

@@ -3,13 +3,13 @@
<div class="page-header">
<h2>Admin Dashboard - User Management</h2>
<button class="btn btn-primary" @click="openCreateModal">
<UserPlus :size="16" />
Create New User
</button>
</div>
<!-- Filters -->
<div class="card">
<h3>Filters</h3>
<CollapsibleSection title="Filters" :icon="Filter">
<div class="filters">
<div class="form-group">
<label for="filter-role">Filter by Role</label>
@@ -31,68 +31,69 @@
/>
</div>
</div>
</div>
</CollapsibleSection>
<!-- Users List -->
<div class="card">
<h3>All Users</h3>
<CollapsibleSection title="All Users" :icon="UsersIcon">
<div v-if="loadingUsers" class="loading">Loading users...</div>
<div v-else-if="users.length === 0" class="empty">
No users found. {{ filterRole || filterOrganization ? 'Try different filters.' : 'Create one above!' }}
</div>
<table v-else class="users-table">
<thead>
<tr>
<th>Email</th>
<th>Full Name</th>
<th>Role</th>
<th>Organization</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.email }}</td>
<td>{{ user.full_name }}</td>
<td>
<span :class="['badge', user.role === 'admin' ? 'badge-admin' : 'badge-user']">
{{ user.role }}
</span>
</td>
<td>{{ user.organization || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-active' : 'badge-inactive']">
{{ user.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="actions">
<button
class="btn btn-sm btn-secondary"
@click="startEdit(user)"
:disabled="loading"
>
Edit
</button>
<button
:class="['btn', 'btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
@click="toggleStatus(user)"
:disabled="loading"
>
{{ user.is_active ? 'Deactivate' : 'Activate' }}
</button>
<button
class="btn btn-sm btn-danger"
@click="showResetPassword(user)"
:disabled="loading"
>
Reset Password
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>Email</th>
<th>Full Name</th>
<th>Role</th>
<th>Organization</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.email }}</td>
<td>{{ user.full_name }}</td>
<td>
<span :class="['badge', user.role === 'admin' ? 'badge-admin' : 'badge-user']">
{{ user.role }}
</span>
</td>
<td>{{ user.organization || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-active' : 'badge-inactive']">
{{ user.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="actions">
<button
class="btn btn-sm btn-secondary"
@click="startEdit(user)"
:disabled="loading"
>
Edit
</button>
<button
:class="['btn', 'btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
@click="toggleStatus(user)"
:disabled="loading"
>
{{ user.is_active ? 'Deactivate' : 'Activate' }}
</button>
<button
class="btn btn-sm btn-danger"
@click="showResetPassword(user)"
:disabled="loading"
>
Reset Password
</button>
</td>
</tr>
</tbody>
</table>
</div>
</CollapsibleSection>
<!-- Create/Edit User Modal -->
<div v-if="showFormModal" class="modal" @click.self="closeFormModal">
@@ -200,6 +201,8 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { usersApi, handleApiError } from '@/services/api'
import CollapsibleSection from '@/components/CollapsibleSection.vue'
import { Users as UsersIcon, UserPlus, Filter } from 'lucide-vue-next'
import type { User } from '@/types'
const users = ref<User[]>([])
@@ -367,11 +370,6 @@ onMounted(() => {
</script>
<style scoped>
.users {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
@@ -383,20 +381,7 @@ onMounted(() => {
.page-header h2 {
margin: 0;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-form {
display: flex;
flex-direction: column;
gap: 16px;
color: var(--color-text-primary);
}
.filters {
@@ -405,6 +390,12 @@ onMounted(() => {
gap: 16px;
}
.user-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
@@ -413,28 +404,31 @@ onMounted(() => {
.form-group label {
font-weight: 500;
color: #374151;
color: var(--color-text-primary);
font-size: 14px;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: 14px;
background: var(--color-surface);
color: var(--color-text-primary);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
border-color: var(--color-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent);
}
.form-group input:disabled {
background: #f3f4f6;
background: var(--color-bg-tertiary);
cursor: not-allowed;
}
@@ -445,13 +439,16 @@ onMounted(() => {
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border: none;
border-radius: 4px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-fast);
}
.btn:disabled {
@@ -460,48 +457,48 @@ onMounted(() => {
}
.btn-primary {
background: #3b82f6;
background: var(--color-accent);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
background: var(--color-accent-hover);
}
.btn-secondary {
background: #6b7280;
color: white;
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: #4b5563;
background: var(--color-border);
}
.btn-success {
background: #10b981;
background: var(--color-success);
color: white;
}
.btn-success:hover:not(:disabled) {
background: #059669;
background: color-mix(in srgb, var(--color-success) 85%, black);
}
.btn-warning {
background: #f59e0b;
background: var(--color-warning);
color: white;
}
.btn-warning:hover:not(:disabled) {
background: #d97706;
background: color-mix(in srgb, var(--color-warning) 85%, black);
}
.btn-danger {
background: #ef4444;
background: var(--color-danger);
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
background: color-mix(in srgb, var(--color-danger) 85%, black);
}
.btn-sm {
@@ -511,53 +508,58 @@ onMounted(() => {
.error {
padding: 12px;
background: #fee2e2;
color: #991b1b;
border-radius: 4px;
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
color: var(--color-danger);
border-radius: var(--radius-sm);
margin-top: 12px;
}
.success {
padding: 12px;
background: #d1fae5;
color: #065f46;
border-radius: 4px;
background: color-mix(in srgb, var(--color-success) 10%, transparent);
color: var(--color-success);
border-radius: var(--radius-sm);
margin-top: 12px;
}
.loading {
text-align: center;
color: #6b7280;
color: var(--color-text-secondary);
padding: 24px;
}
.empty {
text-align: center;
color: #9ca3af;
color: var(--color-text-muted);
padding: 24px;
}
.users-table {
.table-responsive {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.users-table th {
.data-table th {
text-align: left;
padding: 12px;
background: #f9fafb;
background: var(--color-bg-secondary);
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
color: var(--color-text-primary);
border-bottom: 2px solid var(--color-border);
}
.users-table td {
.data-table td {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid var(--color-border);
color: var(--color-text-primary);
}
.users-table tr:hover {
background: #f9fafb;
.data-table tr:hover {
background: var(--color-surface-hover);
}
.badge {
@@ -569,23 +571,23 @@ onMounted(() => {
}
.badge-active {
background: #d1fae5;
color: #065f46;
background: color-mix(in srgb, var(--color-success) 15%, transparent);
color: var(--color-success);
}
.badge-inactive {
background: #fee2e2;
color: #991b1b;
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
color: var(--color-danger);
}
.badge-admin {
background: #dbeafe;
color: #1e40af;
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
color: var(--color-accent);
}
.badge-user {
background: #f3f4f6;
color: #374151;
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.actions {
@@ -608,15 +610,31 @@ onMounted(() => {
}
.modal-content {
background: white;
border-radius: 8px;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: 24px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
box-shadow: var(--shadow-lg);
}
.modal-content h3 {
margin-bottom: 16px;
color: var(--color-text-primary);
}
.collapsible-section + .collapsible-section {
margin-top: 16px;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: stretch;
}
.actions {
flex-direction: column;
}
}
</style>