UI refresh: animation, cards, responsive tweaks

Large frontend modernization: add route fade transition and hoard-soft-enter keyframes with prefers-reduced-motion support; introduce smoother motion tokens and stronger shadow tokens. Update global CSS to use subtle surface gradients, unified transitions, hover lift effects, focus rings and improved button/card/table/overlay styles. Wrap <router-view> in a transition and adjust brand/logo sizing and interactions. Revamp several pages/components (Home, Login, Impressum, 404, Forbidden) with adjusted typography, animated entry for sections and improved card hover states. Admin/Dashboard pages enhanced: AdminUsers gains stats, mobile card list & computed counts; AdminUserDetail and Dashboard show compact summary cards and updated styles. Documentation updated (style.md, codexInfo.md) to reflect the new modernisation rules. No API or backend changes.
This commit is contained in:
Jonas
2026-04-23 22:07:07 +02:00
parent 3f826546ea
commit 10bf4b94ad
13 changed files with 827 additions and 84 deletions
+263 -48
View File
@@ -13,6 +13,10 @@ const errorMessage = ref('')
const users = ref<AdminUser[]>([])
const hasUsers = computed(() => users.value.length > 0)
const activeUserCount = computed(() => users.value.filter((user) => user.isActive).length)
const passwordChangeCount = computed(
() => users.value.filter((user) => user.mustChangePassword).length,
)
function formatRoles(roles: string[]): string {
return roles.length > 0 ? roles.join(', ') : 'Keine Rolle'
@@ -68,6 +72,21 @@ onMounted(() => {
<p>Alle App-Konten mit Rollen, Status und Passwortwechselpflicht.</p>
</header>
<section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht">
<article class="admin-users-stat">
<p class="admin-users-stat-label">Konten</p>
<p class="admin-users-stat-value">{{ users.length }}</p>
</article>
<article class="admin-users-stat">
<p class="admin-users-stat-label">Aktiv</p>
<p class="admin-users-stat-value">{{ activeUserCount }}</p>
</article>
<article class="admin-users-stat">
<p class="admin-users-stat-label">Passwortwechsel</p>
<p class="admin-users-stat-value">{{ passwordChangeCount }}</p>
</article>
</section>
<v-alert
v-if="errorMessage"
type="error"
@@ -84,54 +103,112 @@ onMounted(() => {
<p>Aktuell sind keine Konten vorhanden.</p>
</section>
<div v-else class="admin-users-table-wrap">
<v-table class="admin-users-table">
<thead>
<tr>
<th>Benutzername</th>
<th>Rollen</th>
<th>Aktiv</th>
<th>Passwortwechsel</th>
<th class="admin-users-col-actions">Aktion</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td>
<td>{{ formatRoles(user.roles) }}</td>
<td>
<span
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
</td>
<td>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</span>
</td>
<td class="admin-users-col-actions">
<v-btn
size="small"
variant="outlined"
prepend-icon="mdi-account-details-outline"
@click="openUserDetail(user.id)"
>
Details
</v-btn>
</td>
</tr>
</tbody>
</v-table>
<div v-else class="admin-users-list-region">
<div class="admin-users-table-wrap">
<v-table class="admin-users-table">
<thead>
<tr>
<th>Benutzername</th>
<th>Rollen</th>
<th>Aktiv</th>
<th>Passwortwechsel</th>
<th class="admin-users-col-actions">Aktion</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td>
<td>{{ formatRoles(user.roles) }}</td>
<td>
<span
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
</td>
<td>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</span>
</td>
<td class="admin-users-col-actions">
<v-btn
size="small"
variant="outlined"
prepend-icon="mdi-account-details-outline"
@click="openUserDetail(user.id)"
>
Details
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</div>
<div class="admin-users-mobile-list" aria-label="Benutzerliste">
<article v-for="user in users" :key="user.id" class="admin-users-mobile-card">
<header class="admin-users-mobile-head">
<span class="admin-users-mobile-avatar">
<v-icon icon="mdi-account-outline" size="20" />
</span>
<div>
<p class="admin-users-mobile-label">Benutzer</p>
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
</div>
</header>
<dl class="admin-users-mobile-details">
<div>
<dt>Rollen</dt>
<dd>{{ formatRoles(user.roles) }}</dd>
</div>
<div>
<dt>Aktiv</dt>
<dd>
<span
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
</dd>
</div>
<div>
<dt>Passwortwechsel</dt>
<dd>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</span>
</dd>
</div>
</dl>
<v-btn
variant="outlined"
prepend-icon="mdi-account-details-outline"
block
@click="openUserDetail(user.id)"
>
Details
</v-btn>
</article>
</div>
</div>
<div class="admin-users-actions hoard-action-row">
@@ -179,8 +256,46 @@ onMounted(() => {
color: var(--color-text-secondary);
}
.admin-users-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
}
.admin-users-stat {
padding: var(--space-4);
border: 1px solid color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%);
border-radius: var(--radius-md);
background-color: color-mix(in srgb, var(--color-surface-alt) 58%, var(--color-surface) 42%);
}
.admin-users-stat-label,
.admin-users-stat-value {
margin: 0;
}
.admin-users-stat-label {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.admin-users-stat-value {
color: var(--color-text);
font-size: 1.75rem;
font-weight: 700;
line-height: 1.15;
}
.admin-users-table-wrap {
overflow-x: auto;
border-radius: var(--radius-md);
}
.admin-users-mobile-list {
display: none;
}
.admin-users-table {
@@ -201,9 +316,109 @@ onMounted(() => {
justify-content: flex-end;
}
@media (prefers-reduced-motion: no-preference) {
.admin-users-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) {
.admin-users-shell {
padding: var(--space-4);
overflow: hidden;
}
.admin-users-stats {
grid-template-columns: 1fr;
}
.admin-users-table-wrap {
display: none;
}
.admin-users-mobile-list {
display: grid;
gap: var(--space-3);
}
.admin-users-mobile-card {
display: grid;
gap: var(--space-4);
min-width: 0;
padding: var(--space-4);
border: 1px solid color-mix(in srgb, var(--color-border) 80%, var(--color-surface) 20%);
border-radius: var(--radius-md);
background-color: color-mix(in srgb, var(--color-surface-alt) 58%, var(--color-surface) 42%);
}
.admin-users-mobile-head {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
align-items: center;
min-width: 0;
}
.admin-users-mobile-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--radius-md);
color: var(--color-primary-700);
background-color: color-mix(in srgb, var(--color-primary-100) 82%, var(--color-surface) 18%);
}
.admin-users-mobile-label,
.admin-users-mobile-head h2,
.admin-users-mobile-details {
margin: 0;
}
.admin-users-mobile-label {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.admin-users-mobile-head h2 {
color: var(--color-text);
font-size: 1.1rem;
overflow-wrap: anywhere;
}
.admin-users-mobile-details {
display: grid;
gap: var(--space-3);
}
.admin-users-mobile-details div {
display: grid;
gap: var(--space-1);
padding-bottom: var(--space-3);
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 78%, var(--color-surface) 22%);
}
.admin-users-mobile-details div:last-child {
padding-bottom: 0;
border-bottom: none;
}
.admin-users-mobile-details dt {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.admin-users-mobile-details dd {
margin: 0;
color: var(--color-text);
overflow-wrap: anywhere;
}
.admin-users-actions {