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">
|
||||
<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>
|
||||
|
||||
<v-footer
|
||||
@@ -492,17 +496,29 @@ watch(
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
transition:
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.brand-button:hover .brand-mark {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -603,7 +619,7 @@ watch(
|
||||
|
||||
.drawer-top {
|
||||
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 {
|
||||
@@ -619,7 +635,7 @@ watch(
|
||||
|
||||
.drawer-bottom {
|
||||
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 {
|
||||
@@ -645,6 +661,23 @@ watch(
|
||||
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 {
|
||||
flex: 0 0 auto;
|
||||
height: auto !important;
|
||||
@@ -755,8 +788,13 @@ watch(
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
|
||||
+116
-11
@@ -29,7 +29,8 @@
|
||||
--radius-lg: 14px;
|
||||
|
||||
--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-2: 8px;
|
||||
@@ -53,8 +54,9 @@
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-loose: 1.65;
|
||||
|
||||
--transition-fast: 160ms ease;
|
||||
--page-bg-glow: rgb(183 227 107 / 14%);
|
||||
--transition-fast: 160ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--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-track: #e8ede5;
|
||||
}
|
||||
@@ -86,9 +88,10 @@
|
||||
--color-info: #6aa8de;
|
||||
|
||||
--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-track: #171b22;
|
||||
}
|
||||
@@ -116,7 +119,9 @@ body {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-md);
|
||||
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;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-y: scroll;
|
||||
@@ -184,15 +189,21 @@ p {
|
||||
.v-navigation-drawer .v-list-item {
|
||||
border-radius: var(--radius-md) !important;
|
||||
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 {
|
||||
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 {
|
||||
color: var(--color-primary-700) !important;
|
||||
background-color: var(--color-primary-100) !important;
|
||||
box-shadow: inset 3px 0 0 var(--color-primary-600);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -200,8 +211,17 @@ p {
|
||||
.v-card {
|
||||
border: 1px solid var(--color-border) !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;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
@@ -225,6 +245,12 @@ p {
|
||||
text-transform: none !important;
|
||||
border-radius: var(--radius-md) !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,
|
||||
@@ -235,6 +261,11 @@ p {
|
||||
.v-btn--variant-elevated:not(.v-btn--disabled):hover,
|
||||
.v-btn--variant-flat:not(.v-btn--disabled):hover {
|
||||
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) {
|
||||
@@ -246,15 +277,32 @@ p {
|
||||
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 {
|
||||
border-color: var(--color-border-strong) !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 */
|
||||
.v-input .v-field {
|
||||
border-radius: var(--radius-md) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.v-input .v-field__outline {
|
||||
@@ -266,6 +314,10 @@ p {
|
||||
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 */
|
||||
.v-input .v-field :is(input, textarea, select):focus-visible {
|
||||
outline: none !important;
|
||||
@@ -275,11 +327,19 @@ p {
|
||||
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 */
|
||||
.v-table {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-surface) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.v-table tbody td {
|
||||
border-bottom-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%) !important;
|
||||
}
|
||||
|
||||
/* Status helpers */
|
||||
.hoard-status {
|
||||
display: inline-flex;
|
||||
@@ -325,10 +389,19 @@ p {
|
||||
|
||||
/* Reusable layout helpers for file/productivity pages */
|
||||
.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-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.hoard-toolbar {
|
||||
@@ -347,12 +420,15 @@ p {
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
|
||||
transition: background-color var(--transition-fast);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, var(--color-surface) 15%);
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.hoard-list-row:hover {
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.hoard-list-row.is-selected {
|
||||
@@ -398,7 +474,36 @@ p {
|
||||
}
|
||||
|
||||
*::-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) {
|
||||
|
||||
@@ -145,7 +145,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: clamp(1.8rem, 2vw + 1rem, 2.4rem);
|
||||
font-size: 2.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,17 @@ h1 {
|
||||
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) {
|
||||
.not-found-shell {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -191,7 +202,7 @@ h1 {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.5rem, 7vw, 1.9rem);
|
||||
font-size: 1.85rem;
|
||||
}
|
||||
|
||||
.not-found-text {
|
||||
|
||||
@@ -80,4 +80,10 @@ onMounted(() => {
|
||||
.forbidden-head p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.forbidden-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+95
-5
@@ -88,7 +88,7 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-preview hoard-panel">
|
||||
<div class="hero-preview">
|
||||
<header class="preview-head">
|
||||
<p class="preview-title">Beispielansicht</p>
|
||||
<span class="preview-pill">Workspace</span>
|
||||
@@ -199,7 +199,7 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
|
||||
h1 {
|
||||
margin-bottom: var(--space-4);
|
||||
max-width: 20ch;
|
||||
font-size: clamp(2rem, 2.5vw + 1rem, 3rem);
|
||||
font-size: 3rem;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
@@ -235,7 +235,15 @@ h1 {
|
||||
align-self: center;
|
||||
padding: var(--space-5);
|
||||
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 {
|
||||
@@ -277,6 +285,16 @@ h1 {
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||
border-radius: var(--radius-md);
|
||||
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 {
|
||||
@@ -300,10 +318,38 @@ h1 {
|
||||
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,
|
||||
.section-head h2 {
|
||||
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,
|
||||
@@ -381,6 +427,44 @@ h1 {
|
||||
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) {
|
||||
.hero,
|
||||
.stack-section {
|
||||
@@ -419,6 +503,12 @@ h1 {
|
||||
|
||||
h1 {
|
||||
max-width: none;
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.value-card h2,
|
||||
.section-head h2 {
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +520,7 @@ h1 {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.6rem, 8vw, 2rem);
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
|
||||
@@ -135,7 +135,7 @@ const legalNotes = [
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: clamp(1.9rem, 2.2vw + 1rem, 2.5rem);
|
||||
font-size: 2.45rem;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
@@ -187,7 +187,7 @@ h1 {
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.2rem, 1.2vw + 0.85rem, 1.6rem);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
@@ -250,6 +250,38 @@ h3 {
|
||||
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) {
|
||||
.details-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -289,7 +321,7 @@ h3 {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.55rem, 7vw, 1.95rem);
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
|
||||
@@ -93,7 +93,17 @@ onMounted(() => {
|
||||
|
||||
<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">
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>ID</dt>
|
||||
@@ -186,7 +196,47 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.admin-user-detail-card {
|
||||
display: grid;
|
||||
gap: 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 {
|
||||
@@ -217,6 +267,12 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.admin-user-detail-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.admin-user-detail-shell {
|
||||
padding: var(--space-4);
|
||||
|
||||
@@ -13,6 +13,10 @@ const errorMessage = ref('')
|
||||
const users = ref<AdminUser[]>([])
|
||||
|
||||
const hasUsers = computed(() => users.value.length > 0)
|
||||
const activeUserCount = computed(() => users.value.filter((user) => user.isActive).length)
|
||||
const passwordChangeCount = computed(
|
||||
() => users.value.filter((user) => user.mustChangePassword).length,
|
||||
)
|
||||
|
||||
function formatRoles(roles: string[]): string {
|
||||
return roles.length > 0 ? roles.join(', ') : 'Keine Rolle'
|
||||
@@ -68,6 +72,21 @@ onMounted(() => {
|
||||
<p>Alle App-Konten mit Rollen, Status und Passwortwechselpflicht.</p>
|
||||
</header>
|
||||
|
||||
<section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht">
|
||||
<article class="admin-users-stat">
|
||||
<p class="admin-users-stat-label">Konten</p>
|
||||
<p class="admin-users-stat-value">{{ users.length }}</p>
|
||||
</article>
|
||||
<article class="admin-users-stat">
|
||||
<p class="admin-users-stat-label">Aktiv</p>
|
||||
<p class="admin-users-stat-value">{{ activeUserCount }}</p>
|
||||
</article>
|
||||
<article class="admin-users-stat">
|
||||
<p class="admin-users-stat-label">Passwortwechsel</p>
|
||||
<p class="admin-users-stat-value">{{ passwordChangeCount }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
@@ -84,54 +103,112 @@ onMounted(() => {
|
||||
<p>Aktuell sind keine Konten vorhanden.</p>
|
||||
</section>
|
||||
|
||||
<div v-else class="admin-users-table-wrap">
|
||||
<v-table class="admin-users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Rollen</th>
|
||||
<th>Aktiv</th>
|
||||
<th>Passwortwechsel</th>
|
||||
<th class="admin-users-col-actions">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td>
|
||||
<td>{{ formatRoles(user.roles) }}</td>
|
||||
<td>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
|
||||
]"
|
||||
>
|
||||
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="admin-users-col-actions">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-account-details-outline"
|
||||
@click="openUserDetail(user.id)"
|
||||
>
|
||||
Details
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<div v-else class="admin-users-list-region">
|
||||
<div class="admin-users-table-wrap">
|
||||
<v-table class="admin-users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Rollen</th>
|
||||
<th>Aktiv</th>
|
||||
<th>Passwortwechsel</th>
|
||||
<th class="admin-users-col-actions">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td class="admin-users-cell-user">{{ user.userName || '(ohne Benutzername)' }}</td>
|
||||
<td>{{ formatRoles(user.roles) }}</td>
|
||||
<td>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
|
||||
]"
|
||||
>
|
||||
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="admin-users-col-actions">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-account-details-outline"
|
||||
@click="openUserDetail(user.id)"
|
||||
>
|
||||
Details
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
|
||||
<div class="admin-users-mobile-list" aria-label="Benutzerliste">
|
||||
<article v-for="user in users" :key="user.id" class="admin-users-mobile-card">
|
||||
<header class="admin-users-mobile-head">
|
||||
<span class="admin-users-mobile-avatar">
|
||||
<v-icon icon="mdi-account-outline" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="admin-users-mobile-label">Benutzer</p>
|
||||
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<dl class="admin-users-mobile-details">
|
||||
<div>
|
||||
<dt>Rollen</dt>
|
||||
<dd>{{ formatRoles(user.roles) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Aktiv</dt>
|
||||
<dd>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Passwortwechsel</dt>
|
||||
<dd>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
|
||||
]"
|
||||
>
|
||||
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-account-details-outline"
|
||||
block
|
||||
@click="openUserDetail(user.id)"
|
||||
>
|
||||
Details
|
||||
</v-btn>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-users-actions hoard-action-row">
|
||||
@@ -179,8 +256,46 @@ onMounted(() => {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.admin-users-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-users-stat {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 58%, var(--color-surface) 42%);
|
||||
}
|
||||
|
||||
.admin-users-stat-label,
|
||||
.admin-users-stat-value {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-stat-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-stat-value {
|
||||
color: var(--color-text);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.admin-users-table-wrap {
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.admin-users-mobile-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-users-table {
|
||||
@@ -201,9 +316,109 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.admin-users-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.admin-users-shell {
|
||||
padding: var(--space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-users-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-users-table-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-users-mobile-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-users-mobile-card {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
padding: var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 80%, var(--color-surface) 20%);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 58%, var(--color-surface) 42%);
|
||||
}
|
||||
|
||||
.admin-users-mobile-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-users-mobile-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-primary-700);
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 82%, var(--color-surface) 18%);
|
||||
}
|
||||
|
||||
.admin-users-mobile-label,
|
||||
.admin-users-mobile-head h2,
|
||||
.admin-users-mobile-details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-mobile-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-mobile-head h2 {
|
||||
color: var(--color-text);
|
||||
font-size: 1.1rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-users-mobile-details {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-users-mobile-details div {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 78%, var(--color-surface) 22%);
|
||||
}
|
||||
|
||||
.admin-users-mobile-details div:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-users-mobile-details dt {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-mobile-details dd {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-users-actions {
|
||||
|
||||
@@ -76,7 +76,7 @@ async function handleSubmit() {
|
||||
<header class="change-password-head">
|
||||
<p class="hoard-kicker">Sicherheitsvorgabe</p>
|
||||
<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>
|
||||
|
||||
<v-alert
|
||||
@@ -182,7 +182,7 @@ async function handleSubmit() {
|
||||
.change-password-divider {
|
||||
margin-top: var(--space-1);
|
||||
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 {
|
||||
@@ -200,6 +200,12 @@ async function handleSubmit() {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.change-password-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.change-password-form {
|
||||
gap: var(--space-2);
|
||||
|
||||
@@ -103,7 +103,7 @@ onMounted(() => {
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<v-form class="login-form hoard-panel" @submit.prevent="handleSubmit">
|
||||
<v-form class="login-form" @submit.prevent="handleSubmit">
|
||||
<div class="form-head">
|
||||
<h2>Login</h2>
|
||||
<p>Melde dich mit deinem bestehenden Konto an.</p>
|
||||
@@ -169,7 +169,7 @@ onMounted(() => {
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
max-width: 18ch;
|
||||
font-size: clamp(1.9rem, 2vw + 1rem, 2.6rem);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -196,12 +196,29 @@ h1 {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 80%, var(--color-surface) 20%);
|
||||
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 {
|
||||
@@ -227,6 +244,17 @@ h1 {
|
||||
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) {
|
||||
.login-shell {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -244,7 +272,7 @@ h1 {
|
||||
@media (width <= 600px) {
|
||||
h1 {
|
||||
max-width: none;
|
||||
font-size: clamp(1.55rem, 7vw, 1.95rem);
|
||||
font-size: 1.85rem;
|
||||
}
|
||||
|
||||
.login-intro {
|
||||
|
||||
@@ -15,6 +15,17 @@ const prettyUser = computed(() => {
|
||||
|
||||
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() {
|
||||
isLoading.value = true
|
||||
@@ -63,11 +74,45 @@ onMounted(() => {
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<article v-else class="dashboard-user hoard-panel">
|
||||
<article v-else class="dashboard-user">
|
||||
<template v-if="isLoading">
|
||||
<p class="dashboard-loading">Benutzerdaten werden geladen...</p>
|
||||
</template>
|
||||
<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>
|
||||
<pre class="dashboard-json">{{ prettyUser }}</pre>
|
||||
</template>
|
||||
@@ -117,6 +162,9 @@ onMounted(() => {
|
||||
|
||||
.dashboard-user {
|
||||
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 {
|
||||
@@ -125,6 +173,55 @@ onMounted(() => {
|
||||
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 {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@@ -145,9 +242,53 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.dashboard-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.dashboard-page,
|
||||
.dashboard-shell,
|
||||
.dashboard-user,
|
||||
.dashboard-summary-grid,
|
||||
.dashboard-summary-card {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
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 {
|
||||
|
||||
+12
-1
@@ -29,7 +29,7 @@ Die Seite soll sich optisch zwischen Google Drive und einer modernen self-hosted
|
||||
## 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 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
|
||||
|
||||
@@ -88,6 +88,7 @@ Die Typografie soll neutral, gut lesbar und unauffällig modern sein. Keine deko
|
||||
- Bereichsüberschriften: 18–20 px
|
||||
- kleine Meta-Infos: 12–13 px
|
||||
- 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:**
|
||||
- 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
|
||||
- keine starken farbigen Schatten im Produktivbereich
|
||||
- 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
|
||||
|
||||
@@ -356,6 +358,7 @@ Die folgenden Regeln bilden den aktuellen Responsive-Standard von Hoard und soll
|
||||
8. **Responsive QA vor Abschluss**
|
||||
- Pflicht-Viewports: `360x800`, `390x844`, `768x1024`, `1024x768`, `>=1280`.
|
||||
- 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.
|
||||
|
||||
## Interaktionsprinzipien
|
||||
@@ -458,3 +461,11 @@ Das passt zur Produktidee, weil:
|
||||
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.**
|
||||
|
||||
## 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.
|
||||
|
||||
Reference in New Issue
Block a user