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:
+45
-7
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,7 +103,8 @@ 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">
|
||||||
|
<div class="admin-users-table-wrap">
|
||||||
<v-table class="admin-users-table">
|
<v-table class="admin-users-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -134,6 +154,63 @@ onMounted(() => {
|
|||||||
</v-table>
|
</v-table>
|
||||||
</div>
|
</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">
|
<div class="admin-users-actions hoard-action-row">
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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: 18–20 px
|
- Bereichsüberschriften: 18–20 px
|
||||||
- kleine Meta-Infos: 12–13 px
|
- kleine Meta-Infos: 12–13 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 1–2 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. 160–260 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user