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
+45 -7
View File
@@ -428,7 +428,11 @@ watch(
<v-main class="hoard-main"> <v-main class="hoard-main">
<div class="main-shell"> <div class="main-shell">
<router-view /> <router-view v-slot="{ Component }">
<transition name="route-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div> </div>
<v-footer <v-footer
@@ -492,17 +496,29 @@ watch(
color: inherit; color: inherit;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
border-radius: var(--radius-md);
transition:
color var(--transition-fast),
transform var(--transition-fast);
} }
.brand-mark { .brand-mark {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 54px;
height: 54px;
transition:
transform var(--transition-fast);
}
.brand-button:hover .brand-mark {
transform: translateY(-1px);
} }
.brand-logo { .brand-logo {
width: 46px; width: 58px;
height: 46px; height: 58px;
object-fit: contain; object-fit: contain;
} }
@@ -603,7 +619,7 @@ watch(
.drawer-top { .drawer-top {
padding: var(--space-2) var(--space-4) var(--space-4); padding: var(--space-2) var(--space-4) var(--space-4);
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%); border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, var(--color-surface) 15%);
} }
.drawer-title { .drawer-title {
@@ -619,7 +635,7 @@ watch(
.drawer-bottom { .drawer-bottom {
padding: var(--space-3) var(--space-2) var(--space-4); padding: var(--space-3) var(--space-2) var(--space-4);
border-top: 1px solid color-mix(in srgb, var(--color-border) 90%, white 10%); border-top: 1px solid color-mix(in srgb, var(--color-border) 90%, var(--color-surface) 10%);
} }
.drawer-section-head { .drawer-section-head {
@@ -645,6 +661,23 @@ watch(
padding: var(--space-6); padding: var(--space-6);
} }
.route-fade-enter-active,
.route-fade-leave-active {
transition:
opacity var(--transition-medium),
transform var(--transition-medium);
}
.route-fade-enter-from {
opacity: 0;
transform: translateY(8px);
}
.route-fade-leave-to {
opacity: 0;
transform: translateY(-4px);
}
.hoard-footer { .hoard-footer {
flex: 0 0 auto; flex: 0 0 auto;
height: auto !important; height: auto !important;
@@ -755,8 +788,13 @@ watch(
} }
.brand-logo { .brand-logo {
width: 40px; width: 48px;
height: 40px; height: 48px;
}
.brand-mark {
width: 46px;
height: 46px;
} }
.brand-title { .brand-title {
+116 -11
View File
@@ -29,7 +29,8 @@
--radius-lg: 14px; --radius-lg: 14px;
--shadow-sm: 0 1px 2px rgb(16 24 18 / 6%); --shadow-sm: 0 1px 2px rgb(16 24 18 / 6%);
--shadow-md: 0 6px 18px rgb(16 24 18 / 8%); --shadow-md: 0 8px 22px rgb(16 24 18 / 9%);
--shadow-lg: 0 16px 42px rgb(16 24 18 / 11%);
--space-1: 4px; --space-1: 4px;
--space-2: 8px; --space-2: 8px;
@@ -53,8 +54,9 @@
--line-height-normal: 1.5; --line-height-normal: 1.5;
--line-height-loose: 1.65; --line-height-loose: 1.65;
--transition-fast: 160ms ease; --transition-fast: 160ms cubic-bezier(0.2, 0, 0, 1);
--page-bg-glow: rgb(183 227 107 / 14%); --transition-medium: 220ms cubic-bezier(0.2, 0, 0, 1);
--page-bg-soft: color-mix(in srgb, var(--color-primary-100) 36%, var(--color-bg) 64%);
--scrollbar-thumb: #a6b3a2; --scrollbar-thumb: #a6b3a2;
--scrollbar-track: #e8ede5; --scrollbar-track: #e8ede5;
} }
@@ -86,9 +88,10 @@
--color-info: #6aa8de; --color-info: #6aa8de;
--shadow-sm: 0 1px 2px rgb(0 0 0 / 35%); --shadow-sm: 0 1px 2px rgb(0 0 0 / 35%);
--shadow-md: 0 6px 18px rgb(0 0 0 / 40%); --shadow-md: 0 10px 24px rgb(0 0 0 / 38%);
--shadow-lg: 0 18px 44px rgb(0 0 0 / 45%);
--page-bg-glow: rgb(79 145 72 / 7%); --page-bg-soft: color-mix(in srgb, var(--color-primary-100) 28%, var(--color-bg) 72%);
--scrollbar-thumb: #4f5763; --scrollbar-thumb: #4f5763;
--scrollbar-track: #171b22; --scrollbar-track: #171b22;
} }
@@ -116,7 +119,9 @@ body {
font-family: var(--font-family-sans); font-family: var(--font-family-sans);
font-size: var(--font-size-md); font-size: var(--font-size-md);
color: var(--color-text); color: var(--color-text);
background: radial-gradient(circle at top right, var(--page-bg-glow), transparent 36%), var(--color-bg); background:
linear-gradient(180deg, var(--page-bg-soft) 0, var(--color-bg) 280px),
var(--color-bg);
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
overflow-y: scroll; overflow-y: scroll;
@@ -184,15 +189,21 @@ p {
.v-navigation-drawer .v-list-item { .v-navigation-drawer .v-list-item {
border-radius: var(--radius-md) !important; border-radius: var(--radius-md) !important;
margin: 2px var(--space-2); margin: 2px var(--space-2);
transition:
background-color var(--transition-fast),
color var(--transition-fast),
transform var(--transition-fast);
} }
.v-navigation-drawer .v-list-item:hover { .v-navigation-drawer .v-list-item:hover {
background-color: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface-alt) 55%) !important; background-color: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface-alt) 55%) !important;
transform: translateX(2px);
} }
.v-navigation-drawer .v-list-item--active { .v-navigation-drawer .v-list-item--active {
color: var(--color-primary-700) !important; color: var(--color-primary-700) !important;
background-color: var(--color-primary-100) !important; background-color: var(--color-primary-100) !important;
box-shadow: inset 3px 0 0 var(--color-primary-600);
font-weight: 600; font-weight: 600;
} }
@@ -200,8 +211,17 @@ p {
.v-card { .v-card {
border: 1px solid var(--color-border) !important; border: 1px solid var(--color-border) !important;
border-radius: var(--radius-lg) !important; border-radius: var(--radius-lg) !important;
background-color: var(--color-surface) !important; background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-surface) 92%, var(--color-surface-alt) 8%),
var(--color-surface)
) !important;
box-shadow: var(--shadow-sm) !important; box-shadow: var(--shadow-sm) !important;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
} }
.v-card-title { .v-card-title {
@@ -225,6 +245,12 @@ p {
text-transform: none !important; text-transform: none !important;
border-radius: var(--radius-md) !important; border-radius: var(--radius-md) !important;
font-weight: 500 !important; font-weight: 500 !important;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
color var(--transition-fast),
transform var(--transition-fast) !important;
} }
.v-btn--variant-elevated, .v-btn--variant-elevated,
@@ -235,6 +261,11 @@ p {
.v-btn--variant-elevated:not(.v-btn--disabled):hover, .v-btn--variant-elevated:not(.v-btn--disabled):hover,
.v-btn--variant-flat:not(.v-btn--disabled):hover { .v-btn--variant-flat:not(.v-btn--disabled):hover {
box-shadow: var(--shadow-sm) !important; box-shadow: var(--shadow-sm) !important;
transform: translateY(-1px);
}
.v-btn:not(.v-btn--disabled):active {
transform: translateY(0);
} }
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled) { .v-btn.v-btn--variant-elevated:not(.v-btn--disabled) {
@@ -246,15 +277,32 @@ p {
background-color: var(--color-primary-600) !important; background-color: var(--color-primary-600) !important;
} }
.v-btn.v-btn--disabled {
opacity: 1 !important;
color: var(--color-text-muted) !important;
background-color: color-mix(in srgb, var(--color-surface-alt) 82%, var(--color-surface) 18%) !important;
border-color: var(--color-border) !important;
box-shadow: none !important;
}
.v-btn--variant-outlined { .v-btn--variant-outlined {
border-color: var(--color-border-strong) !important; border-color: var(--color-border-strong) !important;
color: var(--color-text) !important; color: var(--color-text) !important;
} }
.v-btn--variant-outlined:not(.v-btn--disabled):hover,
.v-btn--variant-text:not(.v-btn--disabled):hover {
background-color: color-mix(in srgb, var(--color-primary-100) 38%, transparent) !important;
border-color: color-mix(in srgb, var(--color-primary-600) 52%, var(--color-border-strong) 48%) !important;
}
/* Inputs */ /* Inputs */
.v-input .v-field { .v-input .v-field {
border-radius: var(--radius-md) !important; border-radius: var(--radius-md) !important;
background-color: var(--color-surface) !important; background-color: var(--color-surface) !important;
transition:
background-color var(--transition-fast),
box-shadow var(--transition-fast);
} }
.v-input .v-field__outline { .v-input .v-field__outline {
@@ -266,6 +314,10 @@ p {
color: var(--color-primary-600) !important; color: var(--color-primary-600) !important;
} }
.v-input.v-input--focused .v-field {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-300) 28%, transparent);
}
/* Vuetify steuert den Fokuszustand bereits am Feld; verhindert doppelten Fokusrahmen im Input */ /* Vuetify steuert den Fokuszustand bereits am Feld; verhindert doppelten Fokusrahmen im Input */
.v-input .v-field :is(input, textarea, select):focus-visible { .v-input .v-field :is(input, textarea, select):focus-visible {
outline: none !important; outline: none !important;
@@ -275,11 +327,19 @@ p {
color: var(--color-text-secondary) !important; color: var(--color-text-secondary) !important;
} }
.v-overlay .v-list {
border: 1px solid var(--color-border) !important;
border-radius: var(--radius-md) !important;
background-color: var(--color-surface) !important;
box-shadow: var(--shadow-lg) !important;
}
/* Tables and list-like content */ /* Tables and list-like content */
.v-table { .v-table {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background-color: var(--color-surface) !important; background-color: var(--color-surface) !important;
overflow: hidden;
} }
.v-table thead th { .v-table thead th {
@@ -292,6 +352,10 @@ p {
background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%) !important; background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%) !important;
} }
.v-table tbody td {
border-bottom-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%) !important;
}
/* Status helpers */ /* Status helpers */
.hoard-status { .hoard-status {
display: inline-flex; display: inline-flex;
@@ -325,10 +389,19 @@ p {
/* Reusable layout helpers for file/productivity pages */ /* Reusable layout helpers for file/productivity pages */
.hoard-panel { .hoard-panel {
background-color: var(--color-surface); background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-surface) 93%, var(--color-surface-alt) 7%),
var(--color-surface)
);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
} }
.hoard-toolbar { .hoard-toolbar {
@@ -347,12 +420,15 @@ p {
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%); border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, var(--color-surface) 15%);
transition: background-color var(--transition-fast); transition:
background-color var(--transition-fast),
transform var(--transition-fast);
} }
.hoard-list-row:hover { .hoard-list-row:hover {
background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%); background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%);
transform: translateX(2px);
} }
.hoard-list-row.is-selected { .hoard-list-row.is-selected {
@@ -398,7 +474,36 @@ p {
} }
*::-webkit-scrollbar-thumb:hover { *::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--scrollbar-thumb) 85%, black 15%); background: color-mix(in srgb, var(--scrollbar-thumb) 85%, var(--color-text) 15%);
}
@keyframes hoard-soft-enter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
.v-btn,
.v-navigation-drawer .v-list-item,
.hoard-list-row {
transform: none !important;
}
} }
@media (width <= 960px) { @media (width <= 960px) {
+13 -2
View File
@@ -145,7 +145,7 @@ onBeforeUnmount(() => {
h1 { h1 {
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
font-size: clamp(1.8rem, 2vw + 1rem, 2.4rem); font-size: 2.35rem;
font-weight: 700; font-weight: 700;
} }
@@ -160,6 +160,17 @@ h1 {
align-items: center; align-items: center;
} }
@media (prefers-reduced-motion: no-preference) {
.not-found-visual,
.not-found-content {
animation: hoard-soft-enter 260ms both;
}
.not-found-content {
animation-delay: 80ms;
}
}
@media (width <= 960px) { @media (width <= 960px) {
.not-found-shell { .not-found-shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -191,7 +202,7 @@ h1 {
} }
h1 { h1 {
font-size: clamp(1.5rem, 7vw, 1.9rem); font-size: 1.85rem;
} }
.not-found-text { .not-found-text {
+6
View File
@@ -80,4 +80,10 @@ onMounted(() => {
.forbidden-head p { .forbidden-head p {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
@media (prefers-reduced-motion: no-preference) {
.forbidden-shell {
animation: hoard-soft-enter 260ms both;
}
}
</style> </style>
+95 -5
View File
@@ -88,7 +88,7 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
</div> </div>
</div> </div>
<div class="hero-preview hoard-panel"> <div class="hero-preview">
<header class="preview-head"> <header class="preview-head">
<p class="preview-title">Beispielansicht</p> <p class="preview-title">Beispielansicht</p>
<span class="preview-pill">Workspace</span> <span class="preview-pill">Workspace</span>
@@ -199,7 +199,7 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
h1 { h1 {
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
max-width: 20ch; max-width: 20ch;
font-size: clamp(2rem, 2.5vw + 1rem, 3rem); font-size: 3rem;
line-height: 1.08; line-height: 1.08;
} }
@@ -235,7 +235,15 @@ h1 {
align-self: center; align-self: center;
padding: var(--space-5); padding: var(--space-5);
width: 100%; width: 100%;
background-color: color-mix(in srgb, var(--color-surface-alt) 86%, var(--color-surface) 14%); border: 1px solid color-mix(in srgb, var(--color-border) 76%, var(--color-surface) 24%);
border-radius: var(--radius-lg);
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-surface-alt) 82%, var(--color-surface) 18%),
var(--color-surface)
);
box-shadow: var(--shadow-sm);
} }
.preview-head { .preview-head {
@@ -277,6 +285,16 @@ h1 {
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%); border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background-color: var(--color-surface); background-color: var(--color-surface);
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.preview-row:hover {
border-color: color-mix(in srgb, var(--color-primary-300) 52%, var(--color-border) 48%);
box-shadow: var(--shadow-sm);
transform: translateX(3px);
} }
.row-title { .row-title {
@@ -300,10 +318,38 @@ h1 {
padding: var(--space-5); padding: var(--space-5);
} }
.value-card,
.workflow-card,
.feature-card {
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.value-card:hover,
.workflow-card:hover,
.feature-card:hover {
border-color: color-mix(in srgb, var(--color-primary-300) 48%, var(--color-border) 52%);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
:deep(.value-card > .v-icon),
:deep(.feature-card > .v-icon) {
display: inline-flex;
width: 36px;
height: 36px;
padding: var(--space-2);
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%);
}
.value-card h2, .value-card h2,
.section-head h2 { .section-head h2 {
margin: var(--space-3) 0 var(--space-2); margin: var(--space-3) 0 var(--space-2);
font-size: clamp(1.25rem, 1.2vw + 0.8rem, 1.8rem); font-size: 1.65rem;
} }
.value-card p, .value-card p,
@@ -381,6 +427,44 @@ h1 {
background-color: color-mix(in srgb, var(--color-surface-alt) 75%, var(--color-surface) 25%); background-color: color-mix(in srgb, var(--color-surface-alt) 75%, var(--color-surface) 25%);
} }
@media (prefers-reduced-motion: no-preference) {
.hero-copy,
.hero-preview,
.value-card,
.feature-section,
.workflow-section,
.stack-section {
animation: hoard-soft-enter 260ms both;
}
.hero-preview {
animation-delay: 70ms;
}
.value-card:nth-child(2),
.feature-section,
.workflow-section {
animation-delay: 90ms;
}
.value-card:nth-child(3),
.stack-section {
animation-delay: 130ms;
}
.preview-row {
animation: hoard-soft-enter 240ms both;
}
.preview-row:nth-child(2) {
animation-delay: 70ms;
}
.preview-row:nth-child(3) {
animation-delay: 120ms;
}
}
@media (width <= 1100px) { @media (width <= 1100px) {
.hero, .hero,
.stack-section { .stack-section {
@@ -419,6 +503,12 @@ h1 {
h1 { h1 {
max-width: none; max-width: none;
font-size: 2.25rem;
}
.value-card h2,
.section-head h2 {
font-size: 1.45rem;
} }
} }
@@ -430,7 +520,7 @@ h1 {
} }
h1 { h1 {
font-size: clamp(1.6rem, 8vw, 2rem); font-size: 1.9rem;
} }
.hero-lead { .hero-lead {
+35 -3
View File
@@ -135,7 +135,7 @@ const legalNotes = [
h1 { h1 {
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
font-size: clamp(1.9rem, 2.2vw + 1rem, 2.5rem); font-size: 2.45rem;
} }
.hero-lead { .hero-lead {
@@ -187,7 +187,7 @@ h1 {
h2 { h2 {
margin: 0; margin: 0;
font-size: clamp(1.2rem, 1.2vw + 0.85rem, 1.6rem); font-size: 1.45rem;
} }
.detail-list { .detail-list {
@@ -250,6 +250,38 @@ h3 {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.detail-card,
.note-card {
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
}
.detail-card:hover,
.note-card:hover {
border-color: color-mix(in srgb, var(--color-primary-300) 46%, var(--color-border) 54%);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
@media (prefers-reduced-motion: no-preference) {
.impressum-hero,
.detail-card,
.notes-section {
animation: hoard-soft-enter 260ms both;
}
.detail-card:nth-child(2) {
animation-delay: 80ms;
}
.detail-card:nth-child(3),
.notes-section {
animation-delay: 120ms;
}
}
@media (width <= 1080px) { @media (width <= 1080px) {
.details-grid { .details-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -289,7 +321,7 @@ h3 {
} }
h1 { h1 {
font-size: clamp(1.55rem, 7vw, 1.95rem); font-size: 1.9rem;
} }
.hero-meta { .hero-meta {
+57 -1
View File
@@ -93,7 +93,17 @@ onMounted(() => {
<p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen...</p> <p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen...</p>
<article v-else-if="user" class="admin-user-detail-card hoard-panel"> <article v-else-if="user" class="admin-user-detail-card">
<div class="admin-user-detail-profile">
<span class="admin-user-detail-avatar">
<v-icon icon="mdi-account-outline" size="24" />
</span>
<div>
<p class="admin-user-detail-profile-label">Ausgewähltes Konto</p>
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
</div>
</div>
<dl class="admin-user-detail-grid"> <dl class="admin-user-detail-grid">
<div class="admin-user-detail-item"> <div class="admin-user-detail-item">
<dt>ID</dt> <dt>ID</dt>
@@ -186,7 +196,47 @@ onMounted(() => {
} }
.admin-user-detail-card { .admin-user-detail-card {
display: grid;
gap: var(--space-4);
padding: var(--space-4); 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) 54%, var(--color-surface) 46%);
}
.admin-user-detail-profile {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
align-items: center;
min-width: 0;
}
.admin-user-detail-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
color: var(--color-primary-700);
background-color: color-mix(in srgb, var(--color-primary-100) 82%, var(--color-surface) 18%);
}
.admin-user-detail-profile-label {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.admin-user-detail-profile h2 {
margin: var(--space-1) 0 0;
color: var(--color-text);
font-size: 1.35rem;
overflow-wrap: anywhere;
} }
.admin-user-detail-grid { .admin-user-detail-grid {
@@ -217,6 +267,12 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
} }
@media (prefers-reduced-motion: no-preference) {
.admin-user-detail-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) { @media (width <= 600px) {
.admin-user-detail-shell { .admin-user-detail-shell {
padding: var(--space-4); padding: var(--space-4);
+263 -48
View File
@@ -13,6 +13,10 @@ const errorMessage = ref('')
const users = ref<AdminUser[]>([]) const users = ref<AdminUser[]>([])
const hasUsers = computed(() => users.value.length > 0) 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 { function formatRoles(roles: string[]): string {
return roles.length > 0 ? roles.join(', ') : 'Keine Rolle' return roles.length > 0 ? roles.join(', ') : 'Keine Rolle'
@@ -68,6 +72,21 @@ onMounted(() => {
<p>Alle App-Konten mit Rollen, Status und Passwortwechselpflicht.</p> <p>Alle App-Konten mit Rollen, Status und Passwortwechselpflicht.</p>
</header> </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-alert
v-if="errorMessage" v-if="errorMessage"
type="error" type="error"
@@ -84,54 +103,112 @@ onMounted(() => {
<p>Aktuell sind keine Konten vorhanden.</p> <p>Aktuell sind keine Konten vorhanden.</p>
</section> </section>
<div v-else class="admin-users-table-wrap"> <div v-else class="admin-users-list-region">
<v-table class="admin-users-table"> <div class="admin-users-table-wrap">
<thead> <v-table class="admin-users-table">
<tr> <thead>
<th>Benutzername</th> <tr>
<th>Rollen</th> <th>Benutzername</th>
<th>Aktiv</th> <th>Rollen</th>
<th>Passwortwechsel</th> <th>Aktiv</th>
<th class="admin-users-col-actions">Aktion</th> <th>Passwortwechsel</th>
</tr> <th class="admin-users-col-actions">Aktion</th>
</thead> </tr>
<tbody> </thead>
<tr v-for="user in users" :key="user.id"> <tbody>
<td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td> <tr v-for="user in users" :key="user.id">
<td>{{ formatRoles(user.roles) }}</td> <td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td>
<td> <td>{{ formatRoles(user.roles) }}</td>
<span <td>
:class="[ <span
'hoard-status', :class="[
user.isActive ? 'hoard-status--success' : 'hoard-status--danger', 'hoard-status',
]" user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
> ]"
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }} >
</span> {{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</td> </span>
<td> </td>
<span <td>
:class="[ <span
'hoard-status', :class="[
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info', 'hoard-status',
]" user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
> ]"
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }} >
</span> {{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</td> </span>
<td class="admin-users-col-actions"> </td>
<v-btn <td class="admin-users-col-actions">
size="small" <v-btn
variant="outlined" size="small"
prepend-icon="mdi-account-details-outline" variant="outlined"
@click="openUserDetail(user.id)" prepend-icon="mdi-account-details-outline"
> @click="openUserDetail(user.id)"
Details >
</v-btn> Details
</td> </v-btn>
</tr> </td>
</tbody> </tr>
</v-table> </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>
<div class="admin-users-actions hoard-action-row"> <div class="admin-users-actions hoard-action-row">
@@ -179,8 +256,46 @@ onMounted(() => {
color: var(--color-text-secondary); 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 { .admin-users-table-wrap {
overflow-x: auto; overflow-x: auto;
border-radius: var(--radius-md);
}
.admin-users-mobile-list {
display: none;
} }
.admin-users-table { .admin-users-table {
@@ -201,9 +316,109 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
} }
@media (prefers-reduced-motion: no-preference) {
.admin-users-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) { @media (width <= 600px) {
.admin-users-shell { .admin-users-shell {
padding: var(--space-4); 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 { .admin-users-actions {
@@ -76,7 +76,7 @@ async function handleSubmit() {
<header class="change-password-head"> <header class="change-password-head">
<p class="hoard-kicker">Sicherheitsvorgabe</p> <p class="hoard-kicker">Sicherheitsvorgabe</p>
<h1>Passwort ändern</h1> <h1>Passwort ändern</h1>
<p>Hier kannst du ganz bequem dein Password aktualisieren.</p> <p>Hier kannst du ganz bequem dein Passwort aktualisieren.</p>
</header> </header>
<v-alert <v-alert
@@ -182,7 +182,7 @@ async function handleSubmit() {
.change-password-divider { .change-password-divider {
margin-top: var(--space-1); margin-top: var(--space-1);
margin-bottom: 0; margin-bottom: 0;
border-color: color-mix(in srgb, var(--color-border) 82%, white 18%); border-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%);
} }
.change-password-section-label { .change-password-section-label {
@@ -200,6 +200,12 @@ async function handleSubmit() {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
@media (prefers-reduced-motion: no-preference) {
.change-password-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) { @media (width <= 600px) {
.change-password-form { .change-password-form {
gap: var(--space-2); gap: var(--space-2);
+31 -3
View File
@@ -103,7 +103,7 @@ onMounted(() => {
</ul> </ul>
</aside> </aside>
<v-form class="login-form hoard-panel" @submit.prevent="handleSubmit"> <v-form class="login-form" @submit.prevent="handleSubmit">
<div class="form-head"> <div class="form-head">
<h2>Login</h2> <h2>Login</h2>
<p>Melde dich mit deinem bestehenden Konto an.</p> <p>Melde dich mit deinem bestehenden Konto an.</p>
@@ -169,7 +169,7 @@ onMounted(() => {
h1 { h1 {
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
max-width: 18ch; max-width: 18ch;
font-size: clamp(1.9rem, 2vw + 1rem, 2.6rem); font-size: 2.5rem;
font-weight: 700; font-weight: 700;
} }
@@ -196,12 +196,29 @@ h1 {
font-weight: 500; font-weight: 500;
} }
.login-points :deep(.v-icon) {
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
color: var(--color-primary-700);
background-color: color-mix(in srgb, var(--color-primary-100) 78%, var(--color-surface) 22%);
}
.login-form { .login-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-4); gap: var(--space-4);
padding: var(--space-6); padding: var(--space-6);
border: 1px solid color-mix(in srgb, var(--color-border) 80%, var(--color-surface) 20%);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-surface) 96%, var(--color-surface-alt) 4%),
var(--color-surface)
);
box-shadow: var(--shadow-md);
} }
.form-head h2 { .form-head h2 {
@@ -227,6 +244,17 @@ h1 {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
@media (prefers-reduced-motion: no-preference) {
.login-brand,
.login-form {
animation: hoard-soft-enter 260ms both;
}
.login-form {
animation-delay: 80ms;
}
}
@media (width <= 960px) { @media (width <= 960px) {
.login-shell { .login-shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -244,7 +272,7 @@ h1 {
@media (width <= 600px) { @media (width <= 600px) {
h1 { h1 {
max-width: none; max-width: none;
font-size: clamp(1.55rem, 7vw, 1.95rem); font-size: 1.85rem;
} }
.login-intro { .login-intro {
+142 -1
View File
@@ -15,6 +15,17 @@ const prettyUser = computed(() => {
return JSON.stringify(user.value, null, 2) return JSON.stringify(user.value, null, 2)
}) })
const roleLabel = computed(() => {
if (!user.value || user.value.roles.length === 0) {
return 'Keine Rolle'
}
return user.value.roles.join(', ')
})
const accountStateLabel = computed(() => (user.value?.isActive ? 'Aktiv' : 'Inaktiv'))
const passwordStateLabel = computed(() =>
user.value?.mustChangePassword ? 'Erforderlich' : 'Aktuell',
)
async function loadCurrentUser() { async function loadCurrentUser() {
isLoading.value = true isLoading.value = true
@@ -63,11 +74,45 @@ onMounted(() => {
{{ errorMessage }} {{ errorMessage }}
</v-alert> </v-alert>
<article v-else class="dashboard-user hoard-panel"> <article v-else class="dashboard-user">
<template v-if="isLoading"> <template v-if="isLoading">
<p class="dashboard-loading">Benutzerdaten werden geladen...</p> <p class="dashboard-loading">Benutzerdaten werden geladen...</p>
</template> </template>
<template v-else> <template v-else>
<div class="dashboard-summary-grid">
<article class="dashboard-summary-card">
<span class="dashboard-summary-icon">
<v-icon icon="mdi-account-outline" size="20" />
</span>
<div>
<p class="dashboard-summary-label">Konto</p>
<p class="dashboard-summary-value">{{ user?.userName || 'Unbekannt' }}</p>
</div>
</article>
<article class="dashboard-summary-card">
<span class="dashboard-summary-icon">
<v-icon icon="mdi-shield-account-outline" size="20" />
</span>
<div>
<p class="dashboard-summary-label">Rollen</p>
<p class="dashboard-summary-value">{{ roleLabel }}</p>
</div>
</article>
<article class="dashboard-summary-card">
<span class="dashboard-summary-icon">
<v-icon icon="mdi-lock-check-outline" size="20" />
</span>
<div>
<p class="dashboard-summary-label">Status</p>
<p class="dashboard-summary-value">
{{ accountStateLabel }} · Passwort {{ passwordStateLabel }}
</p>
</div>
</article>
</div>
<p class="dashboard-label">Antwort von <code>GET /auth/me</code>:</p> <p class="dashboard-label">Antwort von <code>GET /auth/me</code>:</p>
<pre class="dashboard-json">{{ prettyUser }}</pre> <pre class="dashboard-json">{{ prettyUser }}</pre>
</template> </template>
@@ -117,6 +162,9 @@ onMounted(() => {
.dashboard-user { .dashboard-user {
padding: var(--space-4); 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%);
} }
.dashboard-label { .dashboard-label {
@@ -125,6 +173,55 @@ onMounted(() => {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
.dashboard-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.dashboard-summary-card {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
min-width: 0;
padding: var(--space-3);
border: 1px solid color-mix(in srgb, var(--color-border) 78%, var(--color-surface) 22%);
border-radius: var(--radius-md);
background-color: var(--color-surface);
}
.dashboard-summary-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
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%);
}
.dashboard-summary-label,
.dashboard-summary-value {
margin: 0;
}
.dashboard-summary-label {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.dashboard-summary-value {
color: var(--color-text);
font-weight: 600;
line-height: 1.35;
overflow-wrap: anywhere;
}
.dashboard-loading { .dashboard-loading {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
@@ -145,9 +242,53 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
} }
@media (prefers-reduced-motion: no-preference) {
.dashboard-shell {
animation: hoard-soft-enter 260ms both;
}
}
@media (width <= 600px) { @media (width <= 600px) {
.dashboard-page,
.dashboard-shell,
.dashboard-user,
.dashboard-summary-grid,
.dashboard-summary-card {
min-width: 0;
max-width: 100%;
}
.dashboard-shell { .dashboard-shell {
padding: var(--space-4); padding: var(--space-4);
overflow: hidden;
}
.dashboard-summary-grid {
grid-template-columns: 1fr;
}
.dashboard-user {
padding: var(--space-3);
overflow: hidden;
}
.dashboard-summary-card {
grid-template-columns: auto minmax(0, 1fr);
}
.dashboard-head p,
.dashboard-label {
overflow-wrap: anywhere;
}
.dashboard-json {
max-width: 100%;
overflow-x: hidden;
padding: var(--space-3);
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
font-size: var(--font-size-xs);
} }
.dashboard-actions { .dashboard-actions {
+12 -1
View File
@@ -29,7 +29,7 @@ Die Seite soll sich optisch zwischen Google Drive und einer modernen self-hosted
## Visuelle Identität ## Visuelle Identität
Die Markenwirkung basiert auf neutralen Flächen mit einem kontrollierten Grün als Wiedererkennungsmerkmal. Das Grün kommt aus dem Logo und steht für Ablage, Struktur, Ruhe und „self-hosted tool“ statt „Social App“. Die Markenwirkung basiert auf neutralen Flächen mit einem kontrollierten Grün als Wiedererkennungsmerkmal. Das Grün kommt aus dem Logo und steht für Ablage, Struktur, Ruhe und „self-hosted tool“ statt „Social App“.
Die App soll **light-first** gestaltet werden. Ein Dark Mode kann später kommen, aber das Grunddesign wird zuerst für helle Oberflächen optimiert. Das spart Aufwand und hält die UI konsistenter. Die App bleibt **light-first**, aber Light- und Dark-Mode sind gleichwertig zu pflegen. Neue Komponenten müssen ihre Farben aus den Design-Tokens beziehen, damit beide Modi ohne Sonderlogik funktionieren.
## Farbpalette ## Farbpalette
@@ -88,6 +88,7 @@ Die Typografie soll neutral, gut lesbar und unauffällig modern sein. Keine deko
- Bereichsüberschriften: 1820 px - Bereichsüberschriften: 1820 px
- kleine Meta-Infos: 1213 px - kleine Meta-Infos: 1213 px
- Zeilenhöhe großzügig halten, besonders in Dateilisten und Formularen - Zeilenhöhe großzügig halten, besonders in Dateilisten und Formularen
- Schriftgrößen nicht mit Viewport-Breite skalieren; responsive Größen über feste Werte in Breakpoints setzen
**Schriftgewicht:** **Schriftgewicht:**
- 400 für normalen Fließtext - 400 für normalen Fließtext
@@ -136,6 +137,7 @@ Sehr zurückhaltend einsetzen. Die App soll stabil und ruhig wirken, nicht schwe
- Dropdowns / Modals: etwas stärker, aber nie dramatisch - Dropdowns / Modals: etwas stärker, aber nie dramatisch
- keine starken farbigen Schatten im Produktivbereich - keine starken farbigen Schatten im Produktivbereich
- grüner Glow nur höchstens im Branding oder auf Marketing-/Login-Flächen - grüner Glow nur höchstens im Branding oder auf Marketing-/Login-Flächen
- Hover-Lift maximal 12 px und nur bei klar interaktiven Flächen oder Demo-Karten
## Komponentenstil ## Komponentenstil
@@ -356,6 +358,7 @@ Die folgenden Regeln bilden den aktuellen Responsive-Standard von Hoard und soll
8. **Responsive QA vor Abschluss** 8. **Responsive QA vor Abschluss**
- Pflicht-Viewports: `360x800`, `390x844`, `768x1024`, `1024x768`, `>=1280`. - Pflicht-Viewports: `360x800`, `390x844`, `768x1024`, `1024x768`, `>=1280`.
- Prüfen: Navigation, Scroll-Verhalten, CTA-Erreichbarkeit, Formular-Bedienbarkeit. - Prüfen: Navigation, Scroll-Verhalten, CTA-Erreichbarkeit, Formular-Bedienbarkeit.
- Light- und Dark-Mode jeweils mindestens auf Desktop und Mobile prüfen.
- Desktop-Regression-Check: bei `>=1024` darf sich das gewollte Desktop-Erscheinungsbild nicht ändern. - Desktop-Regression-Check: bei `>=1024` darf sich das gewollte Desktop-Erscheinungsbild nicht ändern.
## Interaktionsprinzipien ## Interaktionsprinzipien
@@ -458,3 +461,11 @@ Das passt zur Produktidee, weil:
Wenn du bei einer UI-Entscheidung unsicher bist, gilt: Wenn du bei einer UI-Entscheidung unsicher bist, gilt:
**Lieber schlichter als spektakulär. Lieber Google-Drive-artig als Dashboard-artig. Lieber ruhige Flächen und klare Listen als visuelle Effekte. Grün ist Identität, nicht Dekoration.** **Lieber schlichter als spektakulär. Lieber Google-Drive-artig als Dashboard-artig. Lieber ruhige Flächen und klare Listen als visuelle Effekte. Grün ist Identität, nicht Dekoration.**
## Aktuelle Modernisierungsregeln
- Oberflächen dürfen subtile lineare Surface-Verläufe, weichere Schatten und klare Fokusrahmen nutzen.
- Route-Wechsel, Banner und wichtige Seitenbereiche dürfen kurze Fade-/Slide-Animationen verwenden.
- Animationen bleiben ruhig: ca. 160260 ms, keine dauerhaften Bewegungen, `prefers-reduced-motion` beachten.
- Brand- und Navigationsbereiche dürfen etwas hochwertiger wirken, die Arbeitsflächen bleiben funktional und ruhig.
- Zusätzliche UI-Elemente sind erlaubt, solange sie aus vorhandenen Daten entstehen und keine API-Calls oder Funktionen ändern.
- QA immer in Light- und Dark-Mode sowie mobil prüfen; insbesondere Navigation, Formulare, Tabellen und Banner.
+4
View File
@@ -116,9 +116,13 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
- Der Router erzwingt Passwortwechsel global: bei `mustChangePassword=true` erfolgt vor normaler Navigation ein Redirect auf `/password/change`; nach erfolgreicher Änderung führt der Flow auf Login zurück. - Der Router erzwingt Passwortwechsel global: bei `mustChangePassword=true` erfolgt vor normaler Navigation ein Redirect auf `/password/change`; nach erfolgreicher Änderung führt der Flow auf Login zurück.
- Im Account-Menü der Topbar gibt es jetzt zusätzlich zur Abmeldung einen direkten Einstieg auf `Passwort ändern` (Desktop und Mobile). - Im Account-Menü der Topbar gibt es jetzt zusätzlich zur Abmeldung einen direkten Einstieg auf `Passwort ändern` (Desktop und Mobile).
- Sidebar-Navigation trennt adminpflichtige Seiten jetzt in einem eigenen Abschnitt `Admin` (gleicher grüner Kicker-Stil wie `Navigation`), sodass z. B. `Benutzer` nicht mehr direkt neben dem Dashboard steht. - Sidebar-Navigation trennt adminpflichtige Seiten jetzt in einem eigenen Abschnitt `Admin` (gleicher grüner Kicker-Stil wie `Navigation`), sodass z. B. `Benutzer` nicht mehr direkt neben dem Dashboard steht.
- Frontend-Modernisierung ist aktiv: globale Surface-/Motion-Tokens, kurze Route-/Page-Animationen mit `prefers-reduced-motion`, Dark-/Light-taugliche Farbmischungen sowie responsive Dashboard-/Admin-Übersichten ohne neue API-Calls.
- Topbar-Branding nutzt das App-Icon jetzt größer und ohne gerahmten Icon-Container.
- Mobile Auth-Ansichten wurden nachgeschärft: Dashboard bricht JSON-/Statusinhalte ohne horizontales Überlaufen um, Benutzerverwaltung nutzt mobil Karten statt breiter Tabelle; Desktop bleibt unverändert.
## Änderungen durch Codex ## Änderungen durch Codex
- Frontend/UI: App-Shell, globale Design-Patterns und öffentliche Seiten wurden konsolidiert; Auth-Routing mit Guard-Logik, Banner-Stack sowie rollenbasierte Navigation inkl. Dashboard-, 403- und Admin-User-Oberflächen ist umgesetzt. - Frontend/UI: App-Shell, globale Design-Patterns und öffentliche Seiten wurden konsolidiert; Auth-Routing mit Guard-Logik, Banner-Stack sowie rollenbasierte Navigation inkl. Dashboard-, 403- und Admin-User-Oberflächen ist umgesetzt.
- Backend/API: Basis-Endpunkte für Health und Auth wurden auf ASP.NET Identity mit Rollenmodell (admin), `/auth/me` mit `roles`, Admin-User-Listen/Details sowie Passwortwechsel mit Session-Invalidierung erweitert. - Backend/API: Basis-Endpunkte für Health und Auth wurden auf ASP.NET Identity mit Rollenmodell (admin), `/auth/me` mit `roles`, Admin-User-Listen/Details sowie Passwortwechsel mit Session-Invalidierung erweitert.
- Infrastruktur/Build: Frontend-Build liefert nach `API/wwwroot` mit sauberem SPA-Fallback, PostgreSQL läuft über EF-Core-Migrationen beim Start (inkl. Dev-Docker-Stack), und lokale Overrides via `appsettings.custom.json` plus strukturiertes Logging/Swagger (Development) sind integriert. - Infrastruktur/Build: Frontend-Build liefert nach `API/wwwroot` mit sauberem SPA-Fallback, PostgreSQL läuft über EF-Core-Migrationen beim Start (inkl. Dev-Docker-Stack), und lokale Overrides via `appsettings.custom.json` plus strukturiertes Logging/Swagger (Development) sind integriert.
- Frontend/UI-Review: Layout, globale Styles, öffentliche Seiten und Admin-/Dashboard-Ansichten wurden optisch modernisiert; `GUI/style.md` dokumentiert die neuen Modernisierungs-, Motion- und Dark-/Light-Regeln.