Modernize frontend: new design system, redesign all pages
- Convert codexInfo.md to CLAUDE.md as the central project context. - Rewrite GUI/style.md with a modernized, file-first direction (layered surfaces, refined typography, motion budget, ambient gradients). - Refresh global tokens in global.css, page-layouts.css and surface-patterns.css: extended palette (primary 050/800, surface elevated, border subtle), four-tier shadow system, dark-mode parity, new utilities (hoard-chip, hoard-icon-tile, hoard-spotlight, hoard-section-head, hoard-divider-soft, status pulse dot). - Rebuild Layout.vue: premium app shell with brand halo, animated active-indicator, account pill with avatar/initials, drawer footer card, refined banner stack and footer. - Redesign every route while preserving routing and API contracts: Home, Login, ChangePassword, Dashboard, AdminUsers, AdminUserDetail, Impressum, 404, Forbidden. Adds search/admin stats, password hint list, dashboard greeting with avatar, modernized hero/spotlight treatments and consistent mobile layouts (safe-areas, 44/48px tap targets). - Drop @fontsource/roboto import in vuetify.ts and load Inter via the rsms.me CSS in index.html; update Vuetify defaults and palette to match the new tokens.
This commit is contained in:
+10
-6
@@ -1,10 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#1c652f" />
|
||||
<meta name="description" content="Hoard – self-hosted Datei- und Markdown-Workspace im Browser." />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<title>Hoard · Self-hosted Workspace</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+396
-166
@@ -124,7 +124,7 @@ const activeRoute = computed(() => {
|
||||
|
||||
const pageName = computed(() => activeRoute.value?.name ?? 'Hoard')
|
||||
const pageDescription = computed(
|
||||
() => activeRoute.value?.description ?? 'Self-hosted Dateiablage im Browser',
|
||||
() => activeRoute.value?.description ?? 'Self-hosted Datei-Workspace im Browser',
|
||||
)
|
||||
const shouldShowFooter = computed(() => activeRoute.value?.disableFooter !== true)
|
||||
const isDarkTheme = computed(() => theme.global.name.value === 'dark')
|
||||
@@ -134,6 +134,26 @@ const themeLabel = computed(() =>
|
||||
)
|
||||
const isAuthenticated = computed(() => currentUser.value !== null)
|
||||
const userLabel = computed(() => currentUser.value?.userName ?? 'Konto')
|
||||
const userInitials = computed(() => {
|
||||
const name = currentUser.value?.userName?.trim() ?? ''
|
||||
if (name.length === 0) {
|
||||
return 'H'
|
||||
}
|
||||
|
||||
const parts = name.split(/[\s._-]+/).filter((part) => part.length > 0)
|
||||
if (parts.length === 0) {
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
const first = parts[0] as string
|
||||
return first.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
const first = parts[0] as string
|
||||
const second = parts[1] as string
|
||||
return `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
|
||||
})
|
||||
|
||||
function applyTheme(nextTheme: 'light' | 'dark', persist = true) {
|
||||
theme.global.name.value = nextTheme
|
||||
@@ -192,7 +212,10 @@ async function handleLogout() {
|
||||
if (error instanceof AuthRequestError) {
|
||||
appBannersStore.pushError(error.message, 'Abmeldung fehlgeschlagen')
|
||||
} else {
|
||||
appBannersStore.pushError('Abmeldung fehlgeschlagen. Bitte versuche es erneut.', 'Abmeldung fehlgeschlagen')
|
||||
appBannersStore.pushError(
|
||||
'Abmeldung fehlgeschlagen. Bitte versuche es erneut.',
|
||||
'Abmeldung fehlgeschlagen',
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
isLoggingOut.value = false
|
||||
@@ -206,11 +229,11 @@ function dismissBanner(id: string) {
|
||||
|
||||
function changeWebsiteTitle() {
|
||||
if (activeRoute.value) {
|
||||
document.title = `Hoard | ${activeRoute.value.name}`
|
||||
document.title = `Hoard · ${activeRoute.value.name}`
|
||||
return
|
||||
}
|
||||
|
||||
document.title = 'Hoard'
|
||||
document.title = 'Hoard · Self-hosted Workspace'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -238,7 +261,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<v-app class="hoard-shell">
|
||||
<v-app-bar class="hoard-app-bar" elevation="0" height="64">
|
||||
<v-app-bar class="hoard-app-bar" elevation="0" height="68">
|
||||
<template #prepend>
|
||||
<v-app-bar-nav-icon
|
||||
v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'"
|
||||
@@ -248,15 +271,16 @@ watch(
|
||||
|
||||
<button type="button" class="brand-button" @click="navigateToBrandTarget">
|
||||
<span class="brand-mark">
|
||||
<img :src="iconImage" alt="Hoard Icon" class="brand-logo" />
|
||||
<span class="brand-mark__halo" aria-hidden="true" />
|
||||
<img :src="iconImage" alt="Hoard Icon" class="brand-mark__logo" />
|
||||
</span>
|
||||
<span class="brand-text">
|
||||
<span class="brand-title">Hoard</span>
|
||||
<span class="brand-subtitle">Dateien zuerst</span>
|
||||
<span class="brand-subtitle">Self-hosted Workspace</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<v-divider vertical class="mx-3 topbar-context-divider" />
|
||||
<span class="topbar-divider" aria-hidden="true" />
|
||||
|
||||
<div class="page-context">
|
||||
<p class="page-name">{{ pageName }}</p>
|
||||
@@ -270,6 +294,7 @@ watch(
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
:aria-label="themeLabel"
|
||||
v-bind="props"
|
||||
@click="toggleTheme"
|
||||
@@ -286,21 +311,30 @@ watch(
|
||||
open-on-hover
|
||||
:open-on-click="false"
|
||||
location="bottom end"
|
||||
offset="8"
|
||||
offset="10"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
class="account-button"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-account-check-outline"
|
||||
:to="{ name: 'Dashboard' }"
|
||||
<button
|
||||
type="button"
|
||||
class="account-pill"
|
||||
v-bind="props"
|
||||
@click="router.push({ name: 'Dashboard' })"
|
||||
>
|
||||
{{ userLabel }}
|
||||
</v-btn>
|
||||
<span class="account-pill__avatar">{{ userInitials }}</span>
|
||||
<span class="account-pill__label">
|
||||
<span class="account-pill__hint">Angemeldet</span>
|
||||
<span class="account-pill__name">{{ userLabel }}</span>
|
||||
</span>
|
||||
<v-icon class="account-pill__chev">mdi-chevron-down</v-icon>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<v-list density="compact" class="account-menu-list">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-view-dashboard-outline"
|
||||
title="Dashboard"
|
||||
:to="{ name: 'Dashboard' }"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-lock-reset"
|
||||
title="Passwort ändern"
|
||||
@@ -309,21 +343,17 @@ watch(
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-logout"
|
||||
:title="isLoggingOut ? 'Abmelden...' : 'Abmelden'"
|
||||
:title="isLoggingOut ? 'Abmelden …' : 'Abmelden'"
|
||||
:disabled="isLoggingOut"
|
||||
@click="handleLogout"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-menu
|
||||
v-else
|
||||
location="bottom end"
|
||||
offset="8"
|
||||
>
|
||||
<v-menu v-else location="bottom end" offset="10">
|
||||
<template #activator="{ props }">
|
||||
<v-btn icon v-bind="props" :aria-label="`Angemeldet als ${userLabel}`">
|
||||
<v-icon>mdi-account-check-outline</v-icon>
|
||||
<v-btn icon variant="text" v-bind="props" :aria-label="`Angemeldet als ${userLabel}`">
|
||||
<span class="account-pill__avatar account-pill__avatar--mobile">{{ userInitials }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
@@ -341,7 +371,7 @@ watch(
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-logout"
|
||||
:title="isLoggingOut ? 'Abmelden...' : 'Abmelden'"
|
||||
:title="isLoggingOut ? 'Abmelden …' : 'Abmelden'"
|
||||
:disabled="isLoggingOut"
|
||||
@click="handleLogout"
|
||||
/>
|
||||
@@ -350,23 +380,18 @@ watch(
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-tooltip v-if="!display.smAndDown.value" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn variant="outlined" prepend-icon="mdi-account-circle-outline" to="/login" v-bind="props">
|
||||
Konto
|
||||
</v-btn>
|
||||
</template>
|
||||
Zum Login
|
||||
</v-tooltip>
|
||||
<v-btn
|
||||
v-if="!display.smAndDown.value"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-login"
|
||||
to="/login"
|
||||
>
|
||||
Anmelden
|
||||
</v-btn>
|
||||
|
||||
<v-tooltip v-else location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn icon to="/login" v-bind="props" aria-label="Zum Login">
|
||||
<v-icon>mdi-account-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Zum Login
|
||||
</v-tooltip>
|
||||
<v-btn v-else icon variant="text" to="/login" aria-label="Zum Login">
|
||||
<v-icon>mdi-login</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</div>
|
||||
</v-app-bar>
|
||||
@@ -377,7 +402,7 @@ watch(
|
||||
:location="display.mobile.value ? 'bottom' : 'left'"
|
||||
:temporary="display.mobile.value"
|
||||
:permanent="!display.mobile.value"
|
||||
:width="268"
|
||||
:width="276"
|
||||
:elevation="display.mobile.value ? 6 : 0"
|
||||
>
|
||||
<div class="drawer-top">
|
||||
@@ -385,20 +410,20 @@ watch(
|
||||
</div>
|
||||
|
||||
<v-list nav :density="display.mobile.value ? 'default' : 'comfortable'" class="px-1">
|
||||
<template v-for="item in primarySidebarRoutes" :key="item.path">
|
||||
<v-list-item
|
||||
:to="item.path"
|
||||
:active="route.path === item.path"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.name"
|
||||
class="hoard-nav-item"
|
||||
rounded="lg"
|
||||
link
|
||||
/>
|
||||
</template>
|
||||
<v-list-item
|
||||
v-for="item in primarySidebarRoutes"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:active="route.path === item.path"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.name"
|
||||
class="hoard-nav-item"
|
||||
rounded="md"
|
||||
link
|
||||
/>
|
||||
|
||||
<template v-if="adminSidebarRoutes.length > 0">
|
||||
<v-divider class="my-2 mx-2" />
|
||||
<hr class="hoard-divider-soft drawer-section-divider" />
|
||||
<div class="drawer-section-head">
|
||||
<p class="drawer-kicker hoard-kicker hoard-kicker--xs">Admin</p>
|
||||
</div>
|
||||
@@ -411,15 +436,40 @@ watch(
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.name"
|
||||
class="hoard-nav-item"
|
||||
rounded="lg"
|
||||
rounded="md"
|
||||
link
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
<template #append>
|
||||
<div v-if="!isAuthenticated" class="drawer-bottom">
|
||||
<v-btn variant="text" prepend-icon="mdi-home" to="/welcome" :block="display.mobile.value">
|
||||
<div class="drawer-bottom">
|
||||
<div v-if="isAuthenticated" class="drawer-card">
|
||||
<span class="drawer-card__avatar">{{ userInitials }}</span>
|
||||
<div class="drawer-card__body">
|
||||
<p class="drawer-card__hint">Eingeloggt</p>
|
||||
<p class="drawer-card__name">{{ userLabel }}</p>
|
||||
</div>
|
||||
<v-btn
|
||||
v-tooltip="'Abmelden'"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
:aria-label="isLoggingOut ? 'Abmelden …' : 'Abmelden'"
|
||||
:disabled="isLoggingOut"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
v-else
|
||||
variant="text"
|
||||
prepend-icon="mdi-home-outline"
|
||||
to="/welcome"
|
||||
:block="display.mobile.value"
|
||||
>
|
||||
Zur Startseite
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -437,19 +487,29 @@ watch(
|
||||
|
||||
<v-footer
|
||||
v-if="shouldShowFooter"
|
||||
class="hoard-footer d-flex align-center justify-space-between ga-2 flex-wrap py-3"
|
||||
class="hoard-footer"
|
||||
>
|
||||
<div class="footer-links">
|
||||
<v-btn
|
||||
v-for="link in footerRoutes"
|
||||
:key="link.path"
|
||||
:to="link.path"
|
||||
:text="link.name"
|
||||
variant="text"
|
||||
rounded
|
||||
/>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand">
|
||||
<img :src="iconImage" alt="" class="footer-logo" />
|
||||
<span>
|
||||
<strong>Hoard</strong>
|
||||
<span class="footer-tag"> · {{ currentYear }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav class="footer-links" aria-label="Footer-Navigation">
|
||||
<v-btn
|
||||
v-for="link in footerRoutes"
|
||||
:key="link.path"
|
||||
:to="link.path"
|
||||
variant="text"
|
||||
size="small"
|
||||
>
|
||||
{{ link.name }}
|
||||
</v-btn>
|
||||
</nav>
|
||||
</div>
|
||||
<p class="footer-copy">{{ currentYear }} - <strong>Hoard</strong></p>
|
||||
</v-footer>
|
||||
</v-main>
|
||||
|
||||
@@ -484,42 +544,61 @@ watch(
|
||||
}
|
||||
|
||||
.hoard-app-bar {
|
||||
padding-inline: var(--space-2);
|
||||
padding-inline: var(--space-3);
|
||||
}
|
||||
|
||||
/* ---------- Brand ---------- */
|
||||
.brand-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: 0;
|
||||
padding: 4px 6px 4px 4px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.brand-button:hover {
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 38%, transparent);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
transition:
|
||||
transform var(--transition-fast);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.brand-button:hover .brand-mark {
|
||||
transform: translateY(-1px);
|
||||
.brand-mark__halo {
|
||||
position: absolute;
|
||||
inset: -6px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
radial-gradient(
|
||||
closest-side,
|
||||
color-mix(in srgb, var(--color-accent-lime) 35%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
opacity: 0.7;
|
||||
filter: blur(6px);
|
||||
transition: opacity var(--transition-medium);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
.brand-button:hover .brand-mark__halo {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.brand-mark__logo {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 10px rgb(28 101 47 / 22%));
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
@@ -531,14 +610,26 @@ watch(
|
||||
|
||||
.brand-title {
|
||||
color: var(--color-text);
|
||||
font-size: 16px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ---------- Topbar ---------- */
|
||||
.topbar-divider {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
margin-inline: var(--space-4);
|
||||
background:
|
||||
linear-gradient(180deg, transparent, var(--color-border), transparent);
|
||||
}
|
||||
|
||||
.page-context {
|
||||
@@ -548,31 +639,8 @@ watch(
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-context-divider {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.account-button {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
:deep(.account-button .v-btn__content) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-name,
|
||||
.page-description,
|
||||
.drawer-kicker,
|
||||
.drawer-text,
|
||||
.footer-copy {
|
||||
.page-description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -583,7 +651,7 @@ watch(
|
||||
}
|
||||
|
||||
.page-description {
|
||||
max-width: min(360px, 32vw);
|
||||
max-width: min(380px, 32vw);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
white-space: nowrap;
|
||||
@@ -591,7 +659,90 @@ watch(
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (width <= 1360px) {
|
||||
.topbar-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.account-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: 4px var(--space-3) 4px 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: color-mix(in srgb, var(--color-surface) 90%, var(--color-surface-alt) 10%);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
background-color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.account-pill:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 50%, var(--color-border) 50%);
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 32%, var(--color-surface) 68%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.account-pill__avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent-lime) 20%, transparent);
|
||||
}
|
||||
|
||||
.account-pill__avatar--mobile {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.account-pill__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
line-height: 1.1;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.account-pill__hint {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.account-pill__name {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.account-pill__chev {
|
||||
color: var(--color-text-muted);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
@media (width <= 1280px) {
|
||||
.page-description {
|
||||
display: none;
|
||||
}
|
||||
@@ -602,44 +753,37 @@ watch(
|
||||
}
|
||||
|
||||
@media (width <= 1180px) {
|
||||
.topbar-context-divider {
|
||||
.topbar-divider,
|
||||
.page-context {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Drawer ---------- */
|
||||
.hoard-drawer {
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.hoard-drawer--mobile {
|
||||
border-top-left-radius: var(--radius-lg);
|
||||
border-top-right-radius: var(--radius-lg);
|
||||
max-height: min(72vh, 560px);
|
||||
border-top-left-radius: var(--radius-xl);
|
||||
border-top-right-radius: var(--radius-xl);
|
||||
max-height: min(74vh, 580px);
|
||||
}
|
||||
|
||||
.drawer-top {
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, var(--color-surface) 15%);
|
||||
padding: var(--space-3) var(--space-5) var(--space-3);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
margin: var(--space-2) 0 var(--space-1);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-lg);
|
||||
.drawer-kicker {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-text {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.drawer-bottom {
|
||||
padding: var(--space-3) var(--space-2) var(--space-4);
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-border) 90%, var(--color-surface) 10%);
|
||||
.drawer-section-divider {
|
||||
margin: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.drawer-section-head {
|
||||
padding: var(--space-2) var(--space-3) var(--space-1);
|
||||
padding: 0 var(--space-5) var(--space-2);
|
||||
}
|
||||
|
||||
:deep(.hoard-nav-item) {
|
||||
@@ -650,10 +794,68 @@ watch(
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drawer-bottom {
|
||||
padding: var(--space-3) var(--space-3) var(--space-4);
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.drawer-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.drawer-card__avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.drawer-card__body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drawer-card__hint,
|
||||
.drawer-card__name {
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.drawer-card__hint {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.drawer-card__name {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---------- Main ---------- */
|
||||
.hoard-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 64px);
|
||||
min-height: calc(100vh - 68px);
|
||||
}
|
||||
|
||||
.main-shell {
|
||||
@@ -678,29 +880,60 @@ watch(
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
.hoard-footer {
|
||||
flex: 0 0 auto;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
max-height: 88px;
|
||||
padding-inline: var(--space-6);
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.footer-brand strong {
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.footer-tag {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ---------- Banner stack ---------- */
|
||||
.app-banner-stack {
|
||||
position: fixed;
|
||||
right: max(var(--space-4), env(safe-area-inset-right));
|
||||
bottom: max(var(--space-4), env(safe-area-inset-bottom));
|
||||
right: max(var(--space-5), env(safe-area-inset-right));
|
||||
bottom: max(var(--space-5), env(safe-area-inset-bottom));
|
||||
z-index: 2100;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -716,6 +949,7 @@ watch(
|
||||
pointer-events: auto;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
box-shadow: var(--shadow-md);
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
:deep(.app-banner-item .v-alert__overlay),
|
||||
@@ -728,19 +962,19 @@ watch(
|
||||
}
|
||||
|
||||
:deep(.app-banner-item--error) {
|
||||
background-color: color-mix(in srgb, var(--color-danger) 10%, var(--color-surface) 90%) !important;
|
||||
background-color: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface) 88%) !important;
|
||||
}
|
||||
|
||||
:deep(.app-banner-item--warning) {
|
||||
background-color: color-mix(in srgb, var(--color-warning) 10%, var(--color-surface) 90%) !important;
|
||||
background-color: color-mix(in srgb, var(--color-warning) 12%, var(--color-surface) 88%) !important;
|
||||
}
|
||||
|
||||
:deep(.app-banner-item--info) {
|
||||
background-color: color-mix(in srgb, var(--color-info) 10%, var(--color-surface) 90%) !important;
|
||||
background-color: color-mix(in srgb, var(--color-info) 12%, var(--color-surface) 88%) !important;
|
||||
}
|
||||
|
||||
:deep(.app-banner-item--success) {
|
||||
background-color: color-mix(in srgb, var(--color-success) 10%, var(--color-surface) 90%) !important;
|
||||
background-color: color-mix(in srgb, var(--color-success) 12%, var(--color-surface) 88%) !important;
|
||||
}
|
||||
|
||||
.app-banner-title,
|
||||
@@ -775,6 +1009,7 @@ watch(
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (width <= 960px) {
|
||||
.hoard-app-bar {
|
||||
padding-inline:
|
||||
@@ -785,27 +1020,28 @@ watch(
|
||||
.brand-button {
|
||||
min-width: 0;
|
||||
gap: var(--space-2);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
.brand-mark,
|
||||
.brand-mark__logo {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.topbar-context-divider,
|
||||
.topbar-divider,
|
||||
.page-context {
|
||||
display: none;
|
||||
}
|
||||
@@ -819,7 +1055,7 @@ watch(
|
||||
}
|
||||
|
||||
.drawer-bottom {
|
||||
padding: var(--space-2) var(--space-3) calc(var(--space-3) + env(safe-area-inset-bottom));
|
||||
padding: var(--space-3) var(--space-3) calc(var(--space-3) + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
:deep(.drawer-bottom .v-btn) {
|
||||
@@ -830,22 +1066,16 @@ watch(
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hoard-footer {
|
||||
justify-content: center !important;
|
||||
gap: var(--space-2);
|
||||
.footer-inner {
|
||||
justify-content: center;
|
||||
padding-inline:
|
||||
max(var(--space-2), env(safe-area-inset-left))
|
||||
max(var(--space-2), env(safe-area-inset-right));
|
||||
max(var(--space-3), env(safe-area-inset-left))
|
||||
max(var(--space-3), env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.app-banner-stack {
|
||||
@@ -870,7 +1100,7 @@ watch(
|
||||
|
||||
.footer-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
:deep(.footer-links .v-btn) {
|
||||
|
||||
+307
-91
@@ -1,85 +1,143 @@
|
||||
/* =============================================================================
|
||||
Hoard – Globale Tokens, Resets und Vuetify-Anpassungen
|
||||
Quelle: GUI/style.md
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
--color-bg: #f6f8f5;
|
||||
/* Surfaces */
|
||||
--color-bg: #f5f8f2;
|
||||
--color-bg-tint: #eef3ea;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f1f4ef;
|
||||
--color-border: #dce4d8;
|
||||
--color-border-strong: #c7d2c2;
|
||||
--color-surface-alt: #f1f4ee;
|
||||
--color-surface-elevated: #fbfcf8;
|
||||
|
||||
--color-text: #1f2a21;
|
||||
--color-text-secondary: #5f6e62;
|
||||
--color-text-muted: #7d8a80;
|
||||
--color-border: #dce3d6;
|
||||
--color-border-strong: #c5cfbe;
|
||||
--color-border-subtle: #e8ede2;
|
||||
|
||||
/* Text */
|
||||
--color-text: #1a2a1e;
|
||||
--color-text-secondary: #5a6a5e;
|
||||
--color-text-muted: #7b897f;
|
||||
--color-text-on-primary: #ffffff;
|
||||
|
||||
/* Brand */
|
||||
--color-primary-800: #10421e;
|
||||
--color-primary-700: #1c652f;
|
||||
--color-primary-600: #2e7d32;
|
||||
--color-primary-500: #3c8f42;
|
||||
--color-primary-300: #a8d5a2;
|
||||
--color-primary-100: #eaf5e8;
|
||||
--color-primary-050: #f4faf1;
|
||||
--color-accent-lime: #b7e36b;
|
||||
|
||||
/* Status */
|
||||
--color-success: #2e7d32;
|
||||
--color-warning: #b7791f;
|
||||
--color-danger: #c0392b;
|
||||
--color-info: #2f6fb3;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
/* Radius */
|
||||
--radius-xs: 6px;
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 18px;
|
||||
--radius-xl: 22px;
|
||||
--radius-full: 999px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgb(16 24 18 / 6%);
|
||||
--shadow-md: 0 8px 22px rgb(16 24 18 / 9%);
|
||||
--shadow-lg: 0 16px 42px rgb(16 24 18 / 11%);
|
||||
/* Shadows */
|
||||
--shadow-xs: 0 1px 1px rgb(20 30 22 / 4%);
|
||||
--shadow-sm: 0 2px 6px rgb(20 30 22 / 6%);
|
||||
--shadow-md: 0 8px 22px rgb(20 30 22 / 8%);
|
||||
--shadow-lg: 0 18px 44px rgb(20 30 22 / 12%);
|
||||
--shadow-glow: 0 12px 36px rgb(28 101 47 / 18%);
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-7: 28px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* Typography */
|
||||
--font-family-sans:
|
||||
Inter, 'Segoe UI', Roboto, system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue',
|
||||
Arial, sans-serif;
|
||||
'Inter', 'Segoe UI', Roboto, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-mono:
|
||||
'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo,
|
||||
'SF Mono', Consolas, 'Liberation Mono', monospace;
|
||||
|
||||
--font-size-2xs: 11px;
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-md: 14px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 24px;
|
||||
--font-size-lg: 16px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 26px;
|
||||
--font-size-3xl: 32px;
|
||||
--font-size-display: 44px;
|
||||
|
||||
--line-height-tight: 1.3;
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-loose: 1.65;
|
||||
|
||||
/* Motion */
|
||||
--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;
|
||||
--transition-slow: 320ms cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
/* Atmospheric helpers */
|
||||
--page-bg-ambient:
|
||||
radial-gradient(
|
||||
90% 60% at 12% -10%,
|
||||
color-mix(in srgb, var(--color-primary-100) 75%, transparent) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
radial-gradient(
|
||||
80% 60% at 100% 0%,
|
||||
color-mix(in srgb, var(--color-accent-lime) 12%, transparent) 0%,
|
||||
transparent 55%
|
||||
),
|
||||
linear-gradient(180deg, var(--color-bg-tint) 0%, var(--color-bg) 360px);
|
||||
|
||||
--scrollbar-thumb: #b1bcab;
|
||||
--scrollbar-thumb-hover: #95a290;
|
||||
--scrollbar-track: transparent;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #101215;
|
||||
--color-surface: #171a1f;
|
||||
--color-surface-alt: #1d2229;
|
||||
--color-border: #2c333d;
|
||||
--color-border-strong: #3a4350;
|
||||
--color-bg: #0e1115;
|
||||
--color-bg-tint: #11161c;
|
||||
--color-surface: #161a20;
|
||||
--color-surface-alt: #1b2028;
|
||||
--color-surface-elevated: #1f252e;
|
||||
|
||||
--color-text: #ebeff3;
|
||||
--color-text-secondary: #c5ccd4;
|
||||
--color-text-muted: #95a0ad;
|
||||
--color-border: #2a323d;
|
||||
--color-border-strong: #3a4452;
|
||||
--color-border-subtle: #232a34;
|
||||
|
||||
--color-text: #e9eff3;
|
||||
--color-text-secondary: #b6bec8;
|
||||
--color-text-muted: #8b94a0;
|
||||
--color-text-on-primary: #08120a;
|
||||
|
||||
--color-primary-800: #8be194;
|
||||
--color-primary-700: #5fb968;
|
||||
--color-primary-600: #4ea758;
|
||||
--color-primary-500: #3f9148;
|
||||
--color-primary-300: #2e6a37;
|
||||
--color-primary-100: #202822;
|
||||
--color-primary-100: #1f2922;
|
||||
--color-primary-050: #161e19;
|
||||
--color-accent-lime: #b7e36b;
|
||||
|
||||
--color-success: #5fb968;
|
||||
@@ -87,15 +145,30 @@
|
||||
--color-danger: #e07a7a;
|
||||
--color-info: #6aa8de;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgb(0 0 0 / 35%);
|
||||
--shadow-md: 0 10px 24px rgb(0 0 0 / 38%);
|
||||
--shadow-lg: 0 18px 44px rgb(0 0 0 / 45%);
|
||||
--shadow-xs: 0 1px 2px rgb(0 0 0 / 30%);
|
||||
--shadow-sm: 0 2px 6px rgb(0 0 0 / 35%);
|
||||
--shadow-md: 0 10px 26px rgb(0 0 0 / 40%);
|
||||
--shadow-lg: 0 18px 44px rgb(0 0 0 / 48%);
|
||||
--shadow-glow: 0 14px 40px rgb(95 185 104 / 22%);
|
||||
|
||||
--page-bg-soft: color-mix(in srgb, var(--color-primary-100) 28%, var(--color-bg) 72%);
|
||||
--scrollbar-thumb: #4f5763;
|
||||
--scrollbar-track: #171b22;
|
||||
--page-bg-ambient:
|
||||
radial-gradient(
|
||||
90% 60% at 12% -10%,
|
||||
color-mix(in srgb, var(--color-primary-700) 18%, transparent) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
radial-gradient(
|
||||
80% 60% at 100% 0%,
|
||||
color-mix(in srgb, var(--color-accent-lime) 6%, transparent) 0%,
|
||||
transparent 55%
|
||||
),
|
||||
linear-gradient(180deg, var(--color-bg-tint) 0%, var(--color-bg) 360px);
|
||||
|
||||
--scrollbar-thumb: #404a57;
|
||||
--scrollbar-thumb-hover: #5a6573;
|
||||
}
|
||||
|
||||
/* ---------- Reset / base ---------- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -119,16 +192,15 @@ body {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text);
|
||||
background:
|
||||
linear-gradient(180deg, var(--page-bg-soft) 0, var(--color-bg) 280px),
|
||||
var(--color-bg);
|
||||
background: var(--page-bg-ambient);
|
||||
background-attachment: fixed;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: color-mix(in srgb, var(--color-primary-300) 38%, transparent);
|
||||
background-color: color-mix(in srgb, var(--color-primary-300) 42%, transparent);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -151,6 +223,13 @@ h6 {
|
||||
margin: 0 0 var(--space-4);
|
||||
line-height: var(--line-height-tight);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -159,12 +238,18 @@ p {
|
||||
line-height: var(--line-height-loose);
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
:where(a, button, input, textarea, select, [tabindex]):focus-visible {
|
||||
outline: 2px solid var(--color-primary-500);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-xs);
|
||||
}
|
||||
|
||||
/* App shell */
|
||||
/* ---------- App shell ---------- */
|
||||
.v-application {
|
||||
font-family: var(--font-family-sans) !important;
|
||||
color: var(--color-text) !important;
|
||||
@@ -176,45 +261,75 @@ p {
|
||||
}
|
||||
|
||||
.v-app-bar {
|
||||
background-color: var(--color-surface) !important;
|
||||
background-color: color-mix(in srgb, var(--color-surface) 92%, transparent) !important;
|
||||
backdrop-filter: saturate(120%) blur(8px);
|
||||
border-bottom: 1px solid var(--color-border) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer {
|
||||
background-color: var(--color-surface-alt) !important;
|
||||
border-right: 1px solid var(--color-border) !important;
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface-alt) 96%, var(--color-primary-100) 4%),
|
||||
var(--color-surface-alt) 220px
|
||||
);
|
||||
}
|
||||
|
||||
.v-navigation-drawer .v-list-item {
|
||||
position: relative;
|
||||
border-radius: var(--radius-md) !important;
|
||||
margin: 2px var(--space-2);
|
||||
min-height: 44px;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
.v-navigation-drawer .v-list-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 50%;
|
||||
width: 3px;
|
||||
height: 0;
|
||||
background-color: var(--color-primary-600);
|
||||
border-radius: var(--radius-full);
|
||||
transform: translateY(-50%);
|
||||
transition: height var(--transition-medium);
|
||||
}
|
||||
|
||||
.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);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-100) 50%,
|
||||
var(--color-surface-alt) 50%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.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);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-100) 75%,
|
||||
var(--color-surface) 25%
|
||||
) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Surface components */
|
||||
.v-navigation-drawer .v-list-item--active::before {
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
/* ---------- Cards / surfaces ---------- */
|
||||
.v-card {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface) 92%, var(--color-surface-alt) 8%),
|
||||
color-mix(in srgb, var(--color-surface) 96%, var(--color-surface-alt) 4%),
|
||||
var(--color-surface)
|
||||
) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
@@ -226,8 +341,9 @@ p {
|
||||
|
||||
.v-card-title {
|
||||
color: var(--color-text) !important;
|
||||
font-size: var(--font-size-lg) !important;
|
||||
font-size: var(--font-size-xl) !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.v-card-text {
|
||||
@@ -236,14 +352,14 @@ p {
|
||||
|
||||
.v-footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
background-color: color-mix(in srgb, var(--color-surface) 90%, var(--color-bg) 10%) !important;
|
||||
background-color: color-mix(in srgb, var(--color-surface) 92%, var(--color-bg) 8%) !important;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
/* ---------- Buttons ---------- */
|
||||
.v-btn {
|
||||
letter-spacing: 0 !important;
|
||||
text-transform: none !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
font-weight: 500 !important;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
@@ -255,7 +371,7 @@ p {
|
||||
|
||||
.v-btn--variant-elevated,
|
||||
.v-btn--variant-flat {
|
||||
box-shadow: none !important;
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
|
||||
.v-btn--variant-elevated:not(.v-btn--disabled):hover,
|
||||
@@ -268,19 +384,32 @@ p {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled) {
|
||||
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled),
|
||||
.v-btn.v-btn--variant-flat:not(.v-btn--disabled) {
|
||||
color: var(--color-text-on-primary) !important;
|
||||
background-color: var(--color-primary-700) !important;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--color-primary-600),
|
||||
var(--color-primary-700)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled):hover {
|
||||
background-color: var(--color-primary-600) !important;
|
||||
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled):hover,
|
||||
.v-btn.v-btn--variant-flat:not(.v-btn--disabled):hover {
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-primary-600) 90%, var(--color-accent-lime) 10%),
|
||||
var(--color-primary-700)
|
||||
) !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;
|
||||
background:
|
||||
color-mix(in srgb, var(--color-surface-alt) 80%, var(--color-surface) 20%) !important;
|
||||
border-color: var(--color-border) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@@ -288,15 +417,29 @@ p {
|
||||
.v-btn--variant-outlined {
|
||||
border-color: var(--color-border-strong) !important;
|
||||
color: var(--color-text) !important;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-surface) 88%,
|
||||
var(--color-surface-alt) 12%
|
||||
) !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;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-100) 38%,
|
||||
transparent
|
||||
) !important;
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-600) 50%,
|
||||
var(--color-border-strong) 50%
|
||||
) !important;
|
||||
color: var(--color-primary-700) !important;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
/* ---------- Inputs ---------- */
|
||||
.v-input .v-field {
|
||||
border-radius: var(--radius-md) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
@@ -315,10 +458,9 @@ p {
|
||||
}
|
||||
|
||||
.v-input.v-input--focused .v-field {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-300) 28%, transparent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-300) 32%, 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;
|
||||
}
|
||||
@@ -330,11 +472,11 @@ p {
|
||||
.v-overlay .v-list {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
background-color: var(--color-surface-elevated) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
/* Tables and list-like content */
|
||||
/* ---------- Tables ---------- */
|
||||
.v-table {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -346,25 +488,43 @@ p {
|
||||
color: var(--color-text-secondary) !important;
|
||||
font-weight: 600 !important;
|
||||
background-color: var(--color-surface-alt) !important;
|
||||
font-size: var(--font-size-xs) !important;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.v-table tbody tr:hover td {
|
||||
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) 32%,
|
||||
var(--color-surface) 68%
|
||||
) !important;
|
||||
}
|
||||
|
||||
.v-table tbody td {
|
||||
border-bottom-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%) !important;
|
||||
border-bottom-color: var(--color-border-subtle) !important;
|
||||
}
|
||||
|
||||
/* Status helpers */
|
||||
/* ---------- Status pills ---------- */
|
||||
.hoard-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 2px var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
padding: 3px var(--space-2);
|
||||
border-radius: var(--radius-xs);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hoard-status::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: currentcolor;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, currentcolor 22%, transparent);
|
||||
}
|
||||
|
||||
.hoard-status--success {
|
||||
@@ -374,25 +534,35 @@ p {
|
||||
|
||||
.hoard-status--warning {
|
||||
color: var(--color-warning);
|
||||
background-color: color-mix(in srgb, var(--color-warning) 15%, var(--color-surface) 85%);
|
||||
background-color: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface) 84%);
|
||||
}
|
||||
|
||||
.hoard-status--danger {
|
||||
color: var(--color-danger);
|
||||
background-color: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface) 88%);
|
||||
background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface) 86%);
|
||||
}
|
||||
|
||||
.hoard-status--info {
|
||||
color: var(--color-info);
|
||||
background-color: color-mix(in srgb, var(--color-info) 12%, var(--color-surface) 88%);
|
||||
background-color: color-mix(in srgb, var(--color-info) 14%, var(--color-surface) 86%);
|
||||
}
|
||||
|
||||
/* Reusable layout helpers for file/productivity pages */
|
||||
.hoard-status--muted {
|
||||
color: var(--color-text-muted);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 70%, var(--color-surface) 30%);
|
||||
}
|
||||
|
||||
.hoard-status--muted::before {
|
||||
background-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* ---------- Reusable surfaces ---------- */
|
||||
.hoard-panel {
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface) 93%, var(--color-surface-alt) 7%),
|
||||
color-mix(in srgb, var(--color-surface) 95%, var(--color-surface-alt) 5%),
|
||||
var(--color-surface)
|
||||
);
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -404,14 +574,28 @@ p {
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.hoard-panel--elevated {
|
||||
background-color: var(--color-surface-elevated);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.hoard-panel--ghost {
|
||||
background:
|
||||
color-mix(in srgb, var(--color-surface-alt) 60%, var(--color-surface) 40%);
|
||||
border-color: var(--color-border-subtle);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hoard-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background-color: var(--color-surface-alt);
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
|
||||
.hoard-list-row {
|
||||
@@ -420,20 +604,28 @@ 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%, var(--color-surface) 15%);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
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%);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-100) 32%,
|
||||
var(--color-surface) 68%
|
||||
);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.hoard-list-row.is-selected {
|
||||
color: var(--color-primary-700);
|
||||
background-color: var(--color-primary-100);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-primary-100) 70%,
|
||||
var(--color-surface) 30%
|
||||
);
|
||||
}
|
||||
|
||||
.hoard-meta {
|
||||
@@ -442,18 +634,23 @@ p {
|
||||
}
|
||||
|
||||
.hoard-empty-state {
|
||||
padding: var(--space-8) var(--space-6);
|
||||
padding: var(--space-10) var(--space-6);
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.hoard-empty-state h2 {
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: 20px;
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Scrollbar refinement */
|
||||
.hoard-empty-state p {
|
||||
margin: 0 auto;
|
||||
max-width: 44ch;
|
||||
}
|
||||
|
||||
/* ---------- Scrollbar ---------- */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
@@ -470,13 +667,17 @@ p {
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--scrollbar-thumb) 85%, var(--color-text) 15%);
|
||||
background-color: var(--scrollbar-thumb-hover);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* ---------- Animations ---------- */
|
||||
@keyframes hoard-soft-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -489,6 +690,20 @@ p {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hoard-pulse-ring {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary-300) 35%, transparent);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 12px color-mix(in srgb, var(--color-primary-300) 0%, transparent);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
@@ -506,6 +721,7 @@ p {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (width <= 960px) {
|
||||
:root {
|
||||
--hoard-mobile-safe-left: max(var(--space-2), env(safe-area-inset-left));
|
||||
@@ -545,7 +761,7 @@ p {
|
||||
}
|
||||
|
||||
.hoard-empty-state {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
padding: var(--space-8) var(--space-4);
|
||||
}
|
||||
|
||||
.v-navigation-drawer {
|
||||
|
||||
+39
-12
@@ -1,10 +1,4 @@
|
||||
import 'vuetify/styles'
|
||||
import '@fontsource/roboto/100.css'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/500.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
import '@fontsource/roboto/900.css'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
@@ -14,6 +8,33 @@ import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
||||
export default createVuetify({
|
||||
components,
|
||||
directives,
|
||||
defaults: {
|
||||
VBtn: {
|
||||
rounded: 'md',
|
||||
},
|
||||
VTextField: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
color: 'primary',
|
||||
},
|
||||
VTextarea: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
color: 'primary',
|
||||
},
|
||||
VSelect: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
color: 'primary',
|
||||
},
|
||||
VAlert: {
|
||||
variant: 'tonal',
|
||||
border: 'start',
|
||||
},
|
||||
VCard: {
|
||||
rounded: 'lg',
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
defaultTheme: 'light',
|
||||
themes: {
|
||||
@@ -21,9 +42,12 @@ export default createVuetify({
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: '#1C652F',
|
||||
secondary: '#5F6E62',
|
||||
background: '#F6F8F5',
|
||||
'primary-darken-1': '#10421E',
|
||||
secondary: '#5A6A5E',
|
||||
accent: '#B7E36B',
|
||||
background: '#F5F8F2',
|
||||
surface: '#FFFFFF',
|
||||
'surface-variant': '#F1F4EE',
|
||||
success: '#2E7D32',
|
||||
warning: '#B7791F',
|
||||
error: '#C0392B',
|
||||
@@ -33,10 +57,13 @@ export default createVuetify({
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
primary: '#4EA758',
|
||||
secondary: '#A7B0BC',
|
||||
background: '#101215',
|
||||
surface: '#171A1F',
|
||||
primary: '#5FB968',
|
||||
'primary-darken-1': '#4EA758',
|
||||
secondary: '#B6BEC8',
|
||||
accent: '#B7E36B',
|
||||
background: '#0E1115',
|
||||
surface: '#161A20',
|
||||
'surface-variant': '#1B2028',
|
||||
success: '#5FB968',
|
||||
warning: '#D0A34E',
|
||||
error: '#E07A7A',
|
||||
|
||||
@@ -32,7 +32,7 @@ function scheduleAutoRedirect() {
|
||||
|
||||
autoRedirectTimer = setTimeout(() => {
|
||||
void router.replace({ name: redirectRouteName.value })
|
||||
}, 2000)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
async function resolveRedirectTarget() {
|
||||
@@ -73,32 +73,43 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="not-found-page hoard-page hoard-page--centered">
|
||||
<section class="not-found-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
|
||||
<section class="not-found-shell hoard-panel hoard-panel-gradient hoard-spotlight hoard-shell-grid">
|
||||
<div class="not-found-visual">
|
||||
<div class="image-frame">
|
||||
<img :src="notFoundImage" alt="Illustration für eine nicht gefundene Seite" class="not-found-image" />
|
||||
<img
|
||||
:src="notFoundImage"
|
||||
alt="Illustration für eine nicht gefundene Seite"
|
||||
class="not-found-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="not-found-content">
|
||||
<p class="not-found-kicker hoard-kicker hoard-kicker--wide">Fehler 404</p>
|
||||
<h1>Seite nicht gefunden</h1>
|
||||
<p class="not-found-code">404</p>
|
||||
<p class="hoard-kicker hoard-kicker--wide">Seite nicht gefunden</p>
|
||||
<h1>Diese Spur führt ins Leere.</h1>
|
||||
<p class="not-found-text">
|
||||
Der Link ist ungültig oder die Seite wurde verschoben. Du kannst direkt zur
|
||||
{{ autoRedirectTargetLabel }} weitergehen oder die vorherige Ansicht öffnen.
|
||||
Du wirst automatisch dorthin weitergeleitet.
|
||||
Der Link ist ungültig oder die Seite wurde verschoben. Wir leiten dich gleich zur
|
||||
{{ autoRedirectTargetLabel }} weiter – oder du nutzt direkt einen der Buttons unten.
|
||||
</p>
|
||||
|
||||
<div class="not-found-actions hoard-action-row">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:prepend-icon="redirectButtonIcon"
|
||||
@click="navigateToPrimaryTarget"
|
||||
>
|
||||
{{ redirectButtonLabel }}
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">Zurück</v-btn>
|
||||
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">
|
||||
Zurück
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<p class="not-found-hint">
|
||||
<v-icon icon="mdi-clock-time-four-outline" size="14" />
|
||||
Auto-Redirect zur {{ autoRedirectTargetLabel }} in wenigen Sekunden.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</v-container>
|
||||
@@ -106,13 +117,15 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.not-found-shell {
|
||||
--hoard-shell-width: 980px;
|
||||
--hoard-gradient-angle: 180deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-surface) 94%, var(--color-primary-100) 6%);
|
||||
--hoard-gradient-end: color-mix(in srgb, var(--color-surface) 82%, var(--color-surface-alt) 18%);
|
||||
--hoard-gradient-end-stop: 100%;
|
||||
--hoard-shell-width: 1040px;
|
||||
--hoard-gradient-angle: 130deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr);
|
||||
border-radius: var(--radius-xl);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.not-found-visual {
|
||||
@@ -122,13 +135,20 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.image-frame {
|
||||
width: min(100%, 360px);
|
||||
position: relative;
|
||||
width: min(100%, 380px);
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: var(--space-4);
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 84%, var(--color-surface) 16%);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-radius: var(--radius-xl);
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 80% at 0% 100%,
|
||||
color-mix(in srgb, var(--color-primary-100) 50%, transparent),
|
||||
transparent 70%
|
||||
),
|
||||
color-mix(in srgb, var(--color-surface-alt) 86%, var(--color-surface) 14%);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.not-found-image {
|
||||
@@ -143,31 +163,59 @@ onBeforeUnmount(() => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: 2.35rem;
|
||||
.not-found-code {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 88px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 1;
|
||||
background: linear-gradient(135deg, var(--color-primary-700), var(--color-primary-500) 60%, var(--color-accent-lime) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: clamp(1.8rem, 1.4rem + 1vw, 2.4rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.not-found-text {
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 44ch;
|
||||
margin: 0 0 var(--space-5);
|
||||
max-width: 48ch;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.not-found-actions {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.not-found-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: var(--space-5) 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.not-found-hint .v-icon {
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.not-found-visual,
|
||||
.not-found-content {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
animation: hoard-soft-enter 320ms both;
|
||||
}
|
||||
|
||||
.not-found-content {
|
||||
animation-delay: 80ms;
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,23 +244,17 @@ h1 {
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.not-found-shell {
|
||||
--hoard-shell-padding-block-mobile-xs: var(--space-4);
|
||||
--hoard-shell-padding-inline-mobile-xs: var(--space-3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.85rem;
|
||||
.not-found-code {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.not-found-text {
|
||||
margin-bottom: var(--space-4);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.image-frame {
|
||||
width: min(100%, 260px);
|
||||
padding: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.not-found-actions {
|
||||
|
||||
@@ -8,6 +8,9 @@ const isLoading = ref(true)
|
||||
const isAuthenticated = ref(false)
|
||||
|
||||
const primaryActionLabel = computed(() => (isAuthenticated.value ? 'Zum Dashboard' : 'Zum Login'))
|
||||
const primaryActionIcon = computed(() =>
|
||||
isAuthenticated.value ? 'mdi-view-dashboard-outline' : 'mdi-login',
|
||||
)
|
||||
|
||||
async function resolveAuthState() {
|
||||
try {
|
||||
@@ -29,6 +32,15 @@ async function navigatePrimaryAction() {
|
||||
await router.replace({ name: 'Login' })
|
||||
}
|
||||
|
||||
async function navigateBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
|
||||
await router.replace({ name: isAuthenticated.value ? 'Dashboard' : 'Home' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void resolveAuthState()
|
||||
})
|
||||
@@ -36,23 +48,34 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="forbidden-page hoard-page hoard-page--centered">
|
||||
<section class="forbidden-shell hoard-shell-grid hoard-panel">
|
||||
<section class="forbidden-shell hoard-panel hoard-panel-gradient hoard-spotlight">
|
||||
<div class="forbidden-icon">
|
||||
<span class="forbidden-icon__halo" aria-hidden="true" />
|
||||
<v-icon icon="mdi-shield-alert-outline" size="40" />
|
||||
</div>
|
||||
|
||||
<header class="forbidden-head">
|
||||
<p class="hoard-kicker">Fehlende Berechtigung</p>
|
||||
<h1>Kein Zugriff</h1>
|
||||
<p>Dein Konto hat keine ausreichende Rolle für diese Seite.</p>
|
||||
<p class="hoard-kicker hoard-kicker--wide">Fehlende Berechtigung</p>
|
||||
<h1>Kein Zugriff.</h1>
|
||||
<p>
|
||||
Dein Konto hat aktuell keine ausreichende Rolle, um diese Seite zu sehen. Falls das ein Fehler ist,
|
||||
wende dich an einen Admin – oder wechsle zurück in deinen freigegebenen Bereich.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="forbidden-actions hoard-action-row">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-right"
|
||||
variant="elevated"
|
||||
:prepend-icon="primaryActionIcon"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="navigatePrimaryAction"
|
||||
>
|
||||
{{ primaryActionLabel }}
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">
|
||||
Zurück
|
||||
</v-btn>
|
||||
</div>
|
||||
</section>
|
||||
</v-container>
|
||||
@@ -60,30 +83,91 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.forbidden-page {
|
||||
--hoard-shell-width: min(640px, 100%);
|
||||
--hoard-centered-offset: 200px;
|
||||
}
|
||||
|
||||
.forbidden-shell {
|
||||
gap: var(--space-4);
|
||||
--hoard-gradient-angle: 130deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface) 86%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: min(640px, 100%);
|
||||
padding: var(--space-10) var(--space-7);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.forbidden-head h1,
|
||||
.forbidden-head p {
|
||||
margin: 0;
|
||||
.forbidden-icon {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
linear-gradient(135deg, var(--color-warning), color-mix(in srgb, var(--color-warning) 60%, var(--color-danger) 40%));
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
var(--shadow-md),
|
||||
inset 0 0 0 2px color-mix(in srgb, var(--color-accent-lime) 18%, transparent);
|
||||
}
|
||||
|
||||
.forbidden-icon__halo {
|
||||
position: absolute;
|
||||
inset: -10px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
radial-gradient(
|
||||
closest-side,
|
||||
color-mix(in srgb, var(--color-warning) 38%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
filter: blur(12px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.forbidden-head h1 {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
margin: 0 0 var(--space-2);
|
||||
font-size: clamp(1.8rem, 1.4rem + 1vw, 2.4rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.forbidden-head p {
|
||||
margin: 0 auto;
|
||||
max-width: 50ch;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.forbidden-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.forbidden-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.forbidden-shell {
|
||||
padding: var(--space-7) var(--space-5);
|
||||
}
|
||||
|
||||
.forbidden-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.forbidden-actions .v-btn) {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+429
-204
@@ -1,19 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import iconImage from '@/assets/images/icon.svg'
|
||||
|
||||
const valueProps = [
|
||||
{
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
title: 'Dateien zuerst',
|
||||
text: 'Ordner, Dateiliste und Vorschau sind der Kern. Kein überladenes Dashboard-Gefühl.',
|
||||
text: 'Ordner, Dateiliste und Vorschau bilden den Kern – kein überladenes Dashboard, keine Ablenkung.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-file-document-edit-outline',
|
||||
title: 'Markdown direkt im Browser',
|
||||
text: 'Dokumente bearbeiten, lesen und strukturieren ohne Tool-Wechsel.',
|
||||
text: 'Notizen, Doku und Konzepte ohne Tool-Wechsel öffnen, lesen und sauber strukturieren.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-server-outline',
|
||||
title: 'Self-hosted Kontrolle',
|
||||
text: 'Dateimetadaten in PostgreSQL, Dateien in MinIO, alles auf deinem Server.',
|
||||
text: 'Metadaten in PostgreSQL, Dateien in MinIO. Volle Datenhoheit auf deinem eigenen Server.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -26,17 +28,17 @@ const coreFeatures = [
|
||||
{
|
||||
icon: 'mdi-image-outline',
|
||||
title: 'PDF- und Bildvorschau',
|
||||
text: 'Dateien öffnen und direkt einsehen, ohne externe Viewer oder Downloads.',
|
||||
text: 'Dateien direkt einsehen, ohne externe Viewer oder ständige Downloads.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-account-lock-outline',
|
||||
title: 'Klare Benutzerlogik',
|
||||
text: 'Keine offene Registrierung. Accounts werden bewusst und kontrolliert verwaltet.',
|
||||
text: 'Keine offene Registrierung. Konten werden bewusst und kontrolliert verwaltet.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-lightning-bolt-outline',
|
||||
title: 'Schlankes MVP-Setup',
|
||||
text: 'Fokus auf das Wesentliche: stabil, wartbar und realistisch für Solo-Entwicklung.',
|
||||
text: 'Fokus auf das Wesentliche – stabil, wartbar und realistisch für Solo-Entwicklung.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -44,12 +46,12 @@ const workflowSteps = [
|
||||
{
|
||||
number: '01',
|
||||
title: 'Anmelden',
|
||||
text: 'Melde dich mit einem vorhandenen Konto an und starte direkt in deiner Dateiablage.',
|
||||
text: 'Mit deinem bestehenden Konto einsteigen und direkt in deiner Dateiablage starten.',
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
title: 'Dateien strukturieren',
|
||||
text: 'Lege Ordner an, lade Dateien hoch und halte deine Arbeitsbereiche aufgeräumt.',
|
||||
text: 'Ordner anlegen, Inhalte hochladen und Arbeitsbereiche aufgeräumt halten.',
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
@@ -58,93 +60,159 @@ const workflowSteps = [
|
||||
},
|
||||
]
|
||||
|
||||
const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3', 'Cookie Auth']
|
||||
const techStack = [
|
||||
{ label: 'Vue 3', icon: 'mdi-vuejs' },
|
||||
{ label: 'ASP.NET Core', icon: 'mdi-dot-net' },
|
||||
{ label: 'PostgreSQL', icon: 'mdi-database-outline' },
|
||||
{ label: 'MinIO', icon: 'mdi-cloud-outline' },
|
||||
{ label: 'md-editor-v3', icon: 'mdi-language-markdown-outline' },
|
||||
{ label: 'Cookie-Auth', icon: 'mdi-cookie-outline' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid class="landing-page hoard-page">
|
||||
<section class="hero hoard-panel hoard-panel-gradient">
|
||||
<section class="hero hoard-panel hoard-panel-gradient hoard-spotlight">
|
||||
<div class="hero-copy">
|
||||
<p class="hero-kicker hoard-kicker">Self-hosted Datei-Workspace</p>
|
||||
<h1>Hoard ist deine ruhige Startseite für Dateien, Ordner und Markdown.</h1>
|
||||
<p class="hero-kicker hoard-kicker hoard-kicker--wide">Self-hosted Datei-Workspace</p>
|
||||
<h1>
|
||||
Eine ruhige Heimat für deine
|
||||
<span class="hero-accent">Dateien, Ordner und Notizen.</span>
|
||||
</h1>
|
||||
<p class="hero-lead">
|
||||
Eine einfache, Google-Drive-inspirierte Web-App für Teams, die volle Kontrolle über
|
||||
Daten, Struktur und Workflow behalten wollen.
|
||||
Hoard ist eine Google-Drive-inspirierte Web-App für Teams, die volle Kontrolle über
|
||||
Daten, Struktur und Workflow behalten wollen – ohne Cloud-Lock-in, ohne SaaS-Abo.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions hoard-action-row">
|
||||
<v-btn color="primary" size="large" prepend-icon="mdi-login" to="/login">
|
||||
Zum Login
|
||||
<v-btn variant="elevated" size="large" prepend-icon="mdi-login" to="/login">
|
||||
Anmelden
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" size="large" prepend-icon="mdi-file-document-outline" to="/impressum">
|
||||
<v-btn variant="outlined" size="large" prepend-icon="mdi-arrow-right" to="/impressum">
|
||||
Mehr erfahren
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="hero-tags">
|
||||
<span class="hero-tag">Light-first UX</span>
|
||||
<span class="hero-tag">Mehrbenutzerfähig</span>
|
||||
<span class="hero-tag">Ohne SaaS-Abhängigkeit</span>
|
||||
<span class="hoard-chip hoard-chip--brand">
|
||||
<v-icon icon="mdi-shield-check-outline" size="14" /> Datenhoheit
|
||||
</span>
|
||||
<span class="hoard-chip">
|
||||
<v-icon icon="mdi-account-multiple-outline" size="14" /> Mehrbenutzerfähig
|
||||
</span>
|
||||
<span class="hoard-chip">
|
||||
<v-icon icon="mdi-weather-night" size="14" /> Light- und Dark-Mode
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-preview">
|
||||
<header class="preview-head">
|
||||
<p class="preview-title">Beispielansicht</p>
|
||||
<span class="preview-pill">Workspace</span>
|
||||
</header>
|
||||
<div class="preview-list">
|
||||
<article class="preview-row">
|
||||
<v-icon icon="mdi-folder-outline" size="18" />
|
||||
<div>
|
||||
<p class="row-title">Dokumentation</p>
|
||||
<p class="row-meta">Ordner · vor 3 Tagen aktualisiert</p>
|
||||
<aside class="hero-preview" aria-hidden="true">
|
||||
<div class="preview-window">
|
||||
<header class="preview-window__head">
|
||||
<span class="preview-window__dots">
|
||||
<span /><span /><span />
|
||||
</span>
|
||||
<span class="preview-window__path">
|
||||
hoard / dokumentation
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="preview-window__body">
|
||||
<header class="preview-toolbar">
|
||||
<span class="preview-toolbar__title">Dokumentation</span>
|
||||
<span class="hoard-chip hoard-chip--brand">
|
||||
<v-icon icon="mdi-folder-outline" size="14" /> 3 Ordner · 12 Dateien
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="preview-list">
|
||||
<article class="preview-row">
|
||||
<span class="preview-row__icon">
|
||||
<v-icon icon="mdi-folder-outline" size="18" />
|
||||
</span>
|
||||
<div class="preview-row__body">
|
||||
<p class="preview-row__title">Konzepte</p>
|
||||
<p class="preview-row__meta">Ordner · vor 2 Tagen</p>
|
||||
</div>
|
||||
<span class="hoard-status hoard-status--muted">Geteilt</span>
|
||||
</article>
|
||||
|
||||
<article class="preview-row preview-row--active">
|
||||
<span class="preview-row__icon">
|
||||
<v-icon icon="mdi-language-markdown-outline" size="18" />
|
||||
</span>
|
||||
<div class="preview-row__body">
|
||||
<p class="preview-row__title">roadmap.md</p>
|
||||
<p class="preview-row__meta">Markdown · 18 KB</p>
|
||||
</div>
|
||||
<span class="hoard-status hoard-status--success">Editor</span>
|
||||
</article>
|
||||
|
||||
<article class="preview-row">
|
||||
<span class="preview-row__icon">
|
||||
<v-icon icon="mdi-file-pdf-box" size="18" />
|
||||
</span>
|
||||
<div class="preview-row__body">
|
||||
<p class="preview-row__title">api-reference.pdf</p>
|
||||
<p class="preview-row__meta">PDF · 1,2 MB</p>
|
||||
</div>
|
||||
<span class="hoard-status hoard-status--info">Vorschau</span>
|
||||
</article>
|
||||
|
||||
<article class="preview-row">
|
||||
<span class="preview-row__icon">
|
||||
<v-icon icon="mdi-image-outline" size="18" />
|
||||
</span>
|
||||
<div class="preview-row__body">
|
||||
<p class="preview-row__title">screen-2026-04.png</p>
|
||||
<p class="preview-row__meta">Bild · 480 KB</p>
|
||||
</div>
|
||||
<span class="hoard-status hoard-status--muted">Bereit</span>
|
||||
</article>
|
||||
</div>
|
||||
</article>
|
||||
<article class="preview-row">
|
||||
<v-icon icon="mdi-file-document-outline" size="18" />
|
||||
<div>
|
||||
<p class="row-title">roadmap.md</p>
|
||||
<p class="row-meta">Markdown · 18 KB</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="preview-row">
|
||||
<v-icon icon="mdi-file-pdf-box" size="18" />
|
||||
<div>
|
||||
<p class="row-title">api-reference.pdf</p>
|
||||
<p class="row-meta">PDF · 1.2 MB</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-glow" aria-hidden="true">
|
||||
<img :src="iconImage" alt="" class="preview-glow__logo" />
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="value-grid">
|
||||
<article v-for="item in valueProps" :key="item.title" class="value-card hoard-panel">
|
||||
<v-icon :icon="item.icon" size="22" />
|
||||
<span class="hoard-icon-tile hoard-icon-tile--lg">
|
||||
<v-icon :icon="item.icon" size="22" />
|
||||
</span>
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.text }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="feature-section hoard-panel">
|
||||
<header class="section-head">
|
||||
<p class="section-kicker hoard-kicker">Für den Produktivalltag</p>
|
||||
<h2>Weniger Tool-Chaos, mehr Fokus auf Inhalte</h2>
|
||||
<header class="hoard-section-head">
|
||||
<p class="hoard-kicker">Für den Produktivalltag</p>
|
||||
<h2>Weniger Tool-Chaos, mehr Fokus auf Inhalte.</h2>
|
||||
<p>Hoard bündelt Datei- und Markdown-Workflows in einer ruhigen Oberfläche, die nicht ablenkt.</p>
|
||||
</header>
|
||||
|
||||
<div class="feature-grid">
|
||||
<article v-for="feature in coreFeatures" :key="feature.title" class="feature-card">
|
||||
<v-icon :icon="feature.icon" size="20" />
|
||||
<h3>{{ feature.title }}</h3>
|
||||
<p>{{ feature.text }}</p>
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon :icon="feature.icon" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<h3>{{ feature.title }}</h3>
|
||||
<p>{{ feature.text }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workflow-section">
|
||||
<header class="section-head">
|
||||
<p class="section-kicker hoard-kicker">So funktioniert Hoard</p>
|
||||
<h2>In drei klaren Schritten produktiv starten</h2>
|
||||
<header class="hoard-section-head">
|
||||
<p class="hoard-kicker">So funktioniert Hoard</p>
|
||||
<h2>In drei klaren Schritten produktiv starten.</h2>
|
||||
</header>
|
||||
<div class="workflow-grid">
|
||||
<article v-for="step in workflowSteps" :key="step.number" class="workflow-card hoard-panel">
|
||||
@@ -155,17 +223,20 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack-section hoard-panel">
|
||||
<section class="stack-section hoard-panel hoard-panel-gradient">
|
||||
<div class="stack-copy">
|
||||
<p class="section-kicker hoard-kicker">Technische Basis</p>
|
||||
<h2>Schlank gebaut für ein realistisches MVP</h2>
|
||||
<p class="stack-text">
|
||||
<p class="hoard-kicker">Technische Basis</p>
|
||||
<h2>Schlank gebaut für ein realistisches MVP.</h2>
|
||||
<p>
|
||||
Hoard kombiniert einen modernen Frontend-Stack mit einem pragmatischen Backend-Setup,
|
||||
damit Weiterentwicklung und Betrieb auch solo gut machbar bleiben.
|
||||
damit Weiterentwicklung und Betrieb auch solo machbar bleiben.
|
||||
</p>
|
||||
</div>
|
||||
<div class="stack-list">
|
||||
<span v-for="item in techStack" :key="item" class="stack-pill">{{ item }}</span>
|
||||
<span v-for="item in techStack" :key="item.label" class="stack-pill">
|
||||
<v-icon :icon="item.icon" size="16" />
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</v-container>
|
||||
@@ -173,45 +244,59 @@ const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3
|
||||
|
||||
<style scoped>
|
||||
.landing-page {
|
||||
--hoard-page-width: 1180px;
|
||||
--hoard-page-width: 1200px;
|
||||
}
|
||||
|
||||
/* ---------- Hero ---------- */
|
||||
.hero {
|
||||
--hoard-gradient-angle: 120deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 34%, var(--color-surface) 66%);
|
||||
--hoard-gradient-angle: 130deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 52%;
|
||||
--hoard-gradient-end-stop: 60%;
|
||||
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-8);
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
||||
gap: var(--space-10);
|
||||
padding: var(--space-12);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflow-number,
|
||||
.preview-title,
|
||||
.row-title,
|
||||
.row-meta,
|
||||
.stack-text {
|
||||
margin: 0;
|
||||
.hero-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-4);
|
||||
max-width: 20ch;
|
||||
font-size: 3rem;
|
||||
line-height: 1.08;
|
||||
max-width: 18ch;
|
||||
font-size: clamp(2.4rem, 1.6rem + 1.6vw, 3.2rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.hero-accent {
|
||||
background: linear-gradient(120deg, var(--color-primary-700) 30%, var(--color-primary-500) 70%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-bottom: var(--space-5);
|
||||
max-width: 50ch;
|
||||
margin-bottom: var(--space-6);
|
||||
max-width: 56ch;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 15px;
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.hero-tags {
|
||||
@@ -220,56 +305,89 @@ h1 {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.hero-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px var(--space-3);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
/* ---------- Hero preview ---------- */
|
||||
.hero-preview {
|
||||
position: relative;
|
||||
align-self: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-preview {
|
||||
align-self: center;
|
||||
padding: var(--space-5);
|
||||
width: 100%;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 76%, var(--color-surface) 24%);
|
||||
.preview-window {
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: var(--radius-lg);
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface-alt) 82%, var(--color-surface) 18%),
|
||||
color-mix(in srgb, var(--color-surface) 96%, var(--color-surface-alt) 4%),
|
||||
var(--color-surface)
|
||||
);
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-head {
|
||||
.preview-window__head {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background-color: var(--color-surface-alt);
|
||||
}
|
||||
|
||||
.preview-window__dots {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-window__dots span {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: color-mix(in srgb, var(--color-border-strong) 80%, var(--color-text-muted) 20%);
|
||||
}
|
||||
|
||||
.preview-window__dots span:first-child {
|
||||
background-color: color-mix(in srgb, var(--color-danger) 60%, var(--color-border-strong) 40%);
|
||||
}
|
||||
|
||||
.preview-window__dots span:nth-child(2) {
|
||||
background-color: color-mix(in srgb, var(--color-warning) 60%, var(--color-border-strong) 40%);
|
||||
}
|
||||
|
||||
.preview-window__dots span:last-child {
|
||||
background-color: color-mix(in srgb, var(--color-success) 60%, var(--color-border-strong) 40%);
|
||||
}
|
||||
|
||||
.preview-window__path {
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.preview-window__body {
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
.preview-toolbar__title {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px var(--space-2);
|
||||
border-radius: 999px;
|
||||
color: var(--color-primary-700);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -278,44 +396,91 @@ h1 {
|
||||
|
||||
.preview-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
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);
|
||||
background-color 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);
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 50%, var(--color-border) 50%);
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 28%, var(--color-surface) 72%);
|
||||
}
|
||||
|
||||
.row-title {
|
||||
.preview-row--active {
|
||||
border-color: color-mix(in srgb, var(--color-primary-500) 60%, var(--color-border) 40%);
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
|
||||
}
|
||||
|
||||
.preview-row__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 70%, var(--color-surface) 30%);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.preview-row--active .preview-row__icon {
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 80%, var(--color-surface) 20%);
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.preview-row__title,
|
||||
.preview-row__meta {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-row__title {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.row-meta {
|
||||
.preview-row__meta {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.preview-glow {
|
||||
position: absolute;
|
||||
inset: -10% -10% auto auto;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
filter: blur(0.6px);
|
||||
opacity: 0.16;
|
||||
}
|
||||
|
||||
.preview-glow__logo {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ---------- Value grid ---------- */
|
||||
.value-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.value-card {
|
||||
padding: var(--space-5);
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.value-card,
|
||||
@@ -330,42 +495,28 @@ h1 {
|
||||
.value-card:hover,
|
||||
.workflow-card:hover,
|
||||
.feature-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 48%, var(--color-border) 52%);
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 50%, var(--color-border) 50%);
|
||||
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: 1.65rem;
|
||||
.value-card h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.value-card p,
|
||||
.feature-card p,
|
||||
.workflow-card p,
|
||||
.stack-text {
|
||||
.workflow-card p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.feature-section,
|
||||
.stack-section {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
margin-bottom: var(--space-5);
|
||||
/* ---------- Feature section ---------- */
|
||||
.feature-section {
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
@@ -375,40 +526,88 @@ h1 {
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: flex-start;
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface) 80%, var(--color-surface-alt) 20%);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.feature-card h3,
|
||||
.workflow-card h3,
|
||||
.stack-copy h2 {
|
||||
margin: var(--space-2) 0;
|
||||
.workflow-card h3 {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ---------- Workflow ---------- */
|
||||
.workflow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.workflow-card {
|
||||
padding: var(--space-5);
|
||||
position: relative;
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.workflow-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: var(--space-6);
|
||||
top: var(--space-6);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
background: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
|
||||
filter: blur(8px);
|
||||
opacity: 0.7;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.workflow-number {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
color: var(--color-primary-700);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.06em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ---------- Stack ---------- */
|
||||
.stack-section {
|
||||
--hoard-gradient-angle: 110deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 50%, var(--color-surface) 50%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 70%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
||||
gap: var(--space-6);
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||
gap: var(--space-8);
|
||||
align-items: center;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.stack-copy h2 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.stack-copy p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
.stack-list {
|
||||
@@ -418,15 +617,30 @@ h1 {
|
||||
}
|
||||
|
||||
.stack-pill {
|
||||
padding: 6px var(--space-3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 8px var(--space-4);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 75%, var(--color-surface) 25%);
|
||||
box-shadow: var(--shadow-xs);
|
||||
transition: transform var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.stack-pill:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 60%, var(--color-border-strong) 40%);
|
||||
}
|
||||
|
||||
.stack-pill .v-icon {
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
/* ---------- Animations ---------- */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.hero-copy,
|
||||
.hero-preview,
|
||||
@@ -434,22 +648,28 @@ h1 {
|
||||
.feature-section,
|
||||
.workflow-section,
|
||||
.stack-section {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
animation: hoard-soft-enter 320ms both;
|
||||
}
|
||||
|
||||
.hero-preview {
|
||||
animation-delay: 70ms;
|
||||
animation-delay: 80ms;
|
||||
}
|
||||
|
||||
.value-card:nth-child(2),
|
||||
.feature-section,
|
||||
.workflow-section {
|
||||
animation-delay: 90ms;
|
||||
.value-card:nth-child(2) {
|
||||
animation-delay: 80ms;
|
||||
}
|
||||
|
||||
.value-card:nth-child(3),
|
||||
.feature-section {
|
||||
animation-delay: 140ms;
|
||||
}
|
||||
|
||||
.workflow-section {
|
||||
animation-delay: 180ms;
|
||||
}
|
||||
|
||||
.stack-section {
|
||||
animation-delay: 130ms;
|
||||
animation-delay: 220ms;
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
@@ -457,42 +677,53 @@ h1 {
|
||||
}
|
||||
|
||||
.preview-row:nth-child(2) {
|
||||
animation-delay: 70ms;
|
||||
animation-delay: 60ms;
|
||||
}
|
||||
|
||||
.preview-row:nth-child(3) {
|
||||
animation-delay: 120ms;
|
||||
}
|
||||
|
||||
.preview-row:nth-child(4) {
|
||||
animation-delay: 180ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (width <= 1100px) {
|
||||
.hero,
|
||||
.stack-section {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.hero-preview {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.stack-section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.hero,
|
||||
.feature-section,
|
||||
.stack-section {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.hero-preview {
|
||||
padding: var(--space-4);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.preview-window__body {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
padding: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.value-grid,
|
||||
@@ -503,29 +734,26 @@ h1 {
|
||||
|
||||
h1 {
|
||||
max-width: none;
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.value-card h2,
|
||||
.section-head h2 {
|
||||
font-size: 1.45rem;
|
||||
.feature-card {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.hero,
|
||||
.feature-section,
|
||||
.stack-section {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.9rem;
|
||||
.stack-section,
|
||||
.feature-card,
|
||||
.value-card,
|
||||
.workflow-card {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-bottom: var(--space-4);
|
||||
font-size: var(--font-size-md);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
@@ -541,20 +769,17 @@ h1 {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-tag {
|
||||
padding: 4px var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.value-card,
|
||||
.workflow-card,
|
||||
.feature-card {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.stack-pill {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.preview-row .hoard-status {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+138
-129
@@ -19,18 +19,22 @@ const contactDetails = [
|
||||
|
||||
const legalNotes = [
|
||||
{
|
||||
icon: 'mdi-account-voice',
|
||||
title: 'Verantwortlich für den Inhalt',
|
||||
text: 'Julia Beispiel, Musterstraße 42, 12345 Musterstadt, Deutschland (Testdaten).',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-scale-balance',
|
||||
title: 'EU-Streitbeilegung',
|
||||
text: 'Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung bereit: https://ec.europa.eu/consumers/odr/. Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.',
|
||||
text: 'Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung bereit. Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-shield-check-outline',
|
||||
title: 'Haftung für Inhalte',
|
||||
text: 'Als Diensteanbieter sind wir für eigene Inhalte nach den allgemeinen Gesetzen verantwortlich. Für fremde Inhalte, auf die wir verweisen, übernehmen wir keine Gewähr. Dieses Impressum enthält ausschließlich Demo-Angaben.',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-link-variant',
|
||||
title: 'Haftung für Links',
|
||||
text: 'Unsere Seiten enthalten Links zu externen Webseiten Dritter. Für deren Inhalte ist stets der jeweilige Anbieter verantwortlich. Bei Bekanntwerden von Rechtsverletzungen werden derartige Links umgehend entfernt.',
|
||||
},
|
||||
@@ -39,30 +43,39 @@ const legalNotes = [
|
||||
|
||||
<template>
|
||||
<v-container fluid class="impressum-page hoard-page">
|
||||
<section class="impressum-hero hoard-panel hoard-panel-gradient">
|
||||
<div class="hero-copy">
|
||||
<p class="hero-kicker hoard-kicker">Rechtliche Angaben</p>
|
||||
<section class="impressum-hero hoard-panel hoard-panel-gradient hoard-spotlight">
|
||||
<div class="impressum-hero__copy">
|
||||
<p class="hoard-kicker hoard-kicker--wide">Rechtliche Angaben</p>
|
||||
<h1>Impressum</h1>
|
||||
<p class="hero-lead">
|
||||
Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Ersetze die Angaben
|
||||
vor einem produktiven Einsatz mit deinen echten Unternehmensdaten.
|
||||
<p class="impressum-hero__lead">
|
||||
Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Vor produktivem
|
||||
Einsatz bitte alle Angaben durch echte Unternehmensdaten ersetzen.
|
||||
</p>
|
||||
|
||||
<div class="hero-meta">
|
||||
<span class="meta-pill">Testdaten</span>
|
||||
<span class="meta-text">Stand: 17. April 2026</span>
|
||||
<div class="impressum-hero__meta">
|
||||
<span class="hoard-chip hoard-chip--brand">
|
||||
<v-icon icon="mdi-information-outline" size="14" /> Testdaten
|
||||
</span>
|
||||
<span class="hoard-chip">
|
||||
<v-icon icon="mdi-calendar-month-outline" size="14" /> Stand: 26. April 2026
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions hoard-action-row">
|
||||
<v-btn color="primary" prepend-icon="mdi-home" to="/welcome">Zur Startseite</v-btn>
|
||||
<div class="impressum-hero__actions hoard-action-row">
|
||||
<v-btn variant="elevated" prepend-icon="mdi-home-outline" to="/welcome">
|
||||
Zur Startseite
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" prepend-icon="mdi-login" to="/login">Zum Login</v-btn>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="details-grid">
|
||||
<article class="detail-card hoard-panel">
|
||||
<h2>Anbieterangaben</h2>
|
||||
<header class="detail-card__head">
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-domain" size="20" /></span>
|
||||
<h2>Anbieterangaben</h2>
|
||||
</header>
|
||||
<dl class="detail-list">
|
||||
<div v-for="entry in companyDetails" :key="entry.label" class="detail-item">
|
||||
<dt>{{ entry.label }}</dt>
|
||||
@@ -72,7 +85,10 @@ const legalNotes = [
|
||||
</article>
|
||||
|
||||
<article class="detail-card hoard-panel">
|
||||
<h2>Kontakt</h2>
|
||||
<header class="detail-card__head">
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-email-outline" size="20" /></span>
|
||||
<h2>Kontakt</h2>
|
||||
</header>
|
||||
<dl class="detail-list">
|
||||
<div v-for="entry in contactDetails" :key="entry.label" class="detail-item">
|
||||
<dt>{{ entry.label }}</dt>
|
||||
@@ -84,7 +100,10 @@ const legalNotes = [
|
||||
</article>
|
||||
|
||||
<article class="detail-card hoard-panel">
|
||||
<h2>Register und Steuer</h2>
|
||||
<header class="detail-card__head">
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-clipboard-text-outline" size="20" /></span>
|
||||
<h2>Register & Steuer</h2>
|
||||
</header>
|
||||
<dl class="detail-list">
|
||||
<div v-for="entry in registerDetails" :key="entry.label" class="detail-item">
|
||||
<dt>{{ entry.label }}</dt>
|
||||
@@ -95,15 +114,21 @@ const legalNotes = [
|
||||
</section>
|
||||
|
||||
<section class="notes-section hoard-panel">
|
||||
<header class="notes-head">
|
||||
<p class="notes-kicker hoard-kicker">Rechtliche Hinweise</p>
|
||||
<header class="hoard-section-head">
|
||||
<p class="hoard-kicker">Rechtliche Hinweise</p>
|
||||
<h2>Wichtige Zusatzinformationen</h2>
|
||||
<p>Standardklauseln, die im Produktivbetrieb durch eine juristische Prüfung ersetzt werden sollten.</p>
|
||||
</header>
|
||||
|
||||
<div class="notes-grid">
|
||||
<article v-for="note in legalNotes" :key="note.title" class="note-card">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<p>{{ note.text }}</p>
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon :icon="note.icon" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<h3>{{ note.title }}</h3>
|
||||
<p>{{ note.text }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -112,66 +137,50 @@ const legalNotes = [
|
||||
|
||||
<style scoped>
|
||||
.impressum-page {
|
||||
--hoard-page-width: 1120px;
|
||||
--hoard-page-width: 1180px;
|
||||
}
|
||||
|
||||
/* ---------- Hero ---------- */
|
||||
.impressum-hero {
|
||||
--hoard-gradient-angle: 125deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 40%, var(--color-surface) 60%);
|
||||
--hoard-gradient-angle: 130deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 56%;
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1.2fr) auto;
|
||||
gap: var(--space-6);
|
||||
align-items: end;
|
||||
padding: var(--space-8);
|
||||
padding: var(--space-10) var(--space-8);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.hero-lead,
|
||||
.meta-text {
|
||||
.impressum-hero__copy h1 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: clamp(2rem, 1.4rem + 1.4vw, 2.6rem);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.impressum-hero__lead {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: 2.45rem;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
max-width: 64ch;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
.impressum-hero__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
.meta-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px var(--space-3);
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary-300) 60%, var(--color-border) 40%);
|
||||
border-radius: 999px;
|
||||
color: var(--color-primary-700);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 82%, var(--color-surface) 18%);
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
.impressum-hero__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ---------- Detail cards ---------- */
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -181,13 +190,30 @@ h1 {
|
||||
.detail-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-5);
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
h2 {
|
||||
.detail-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 50%, var(--color-border) 50%);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.detail-card__head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.detail-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1.45rem;
|
||||
font-size: var(--font-size-xl);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
@@ -199,7 +225,7 @@ h2 {
|
||||
|
||||
.detail-item {
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
@@ -210,21 +236,21 @@ h2 {
|
||||
dt {
|
||||
margin-bottom: var(--space-1);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
/* ---------- Notes ---------- */
|
||||
.notes-section {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.notes-head {
|
||||
margin-bottom: var(--space-5);
|
||||
padding: var(--space-7);
|
||||
}
|
||||
|
||||
.notes-grid {
|
||||
@@ -234,15 +260,31 @@ dd {
|
||||
}
|
||||
|
||||
.note-card {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 76%, var(--color-surface) 24%);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: flex-start;
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 72%, var(--color-surface) 28%);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 60%, var(--color-surface) 40%);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
background-color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 var(--space-2);
|
||||
font-size: 1.03rem;
|
||||
.note-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 40%, var(--color-border) 60%);
|
||||
background-color: var(--color-surface);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.note-card h3 {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.note-card p {
|
||||
@@ -250,36 +292,16 @@ 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;
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
|
||||
.detail-card:nth-child(2) {
|
||||
animation-delay: 80ms;
|
||||
}
|
||||
|
||||
.detail-card:nth-child(3),
|
||||
.notes-section {
|
||||
animation-delay: 120ms;
|
||||
}
|
||||
.detail-card:nth-child(2) { animation-delay: 80ms; }
|
||||
.detail-card:nth-child(3) { animation-delay: 140ms; }
|
||||
.notes-section { animation-delay: 200ms; }
|
||||
}
|
||||
|
||||
@media (width <= 1080px) {
|
||||
@@ -289,19 +311,14 @@ h3 {
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.impressum-hero,
|
||||
.notes-section {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.impressum-hero {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
padding: var(--space-7);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
.impressum-hero__actions {
|
||||
justify-content: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.details-grid,
|
||||
@@ -309,40 +326,32 @@ h3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
:deep(.hero-actions .v-btn) {
|
||||
min-height: 44px;
|
||||
.notes-section {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.impressum-hero,
|
||||
.notes-section {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
margin-top: var(--space-4);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.hero-actions .v-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notes-section,
|
||||
.detail-card {
|
||||
padding: var(--space-4);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.impressum-hero__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.impressum-hero__actions .v-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.impressum-hero__meta {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.note-card {
|
||||
padding: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,27 @@ const routeUserId = computed(() => {
|
||||
return typeof value === 'string' ? value : ''
|
||||
})
|
||||
|
||||
const userInitials = computed(() => {
|
||||
const name = user.value?.userName?.trim() ?? ''
|
||||
if (name.length === 0) {
|
||||
return '·'
|
||||
}
|
||||
|
||||
const parts = name.split(/[\s._-]+/).filter((part) => part.length > 0)
|
||||
if (parts.length === 0) {
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
const first = parts[0] as string
|
||||
return first.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
const first = parts[0] as string
|
||||
const second = parts[1] as string
|
||||
return `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
|
||||
})
|
||||
|
||||
const rolesText = computed(() => {
|
||||
if (!user.value || user.value.roles.length === 0) {
|
||||
return 'Keine Rolle'
|
||||
@@ -75,78 +96,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="admin-user-detail-page hoard-page">
|
||||
<section class="admin-user-detail-shell hoard-panel">
|
||||
<header class="admin-user-detail-head">
|
||||
<p class="hoard-kicker">Adminbereich</p>
|
||||
<header class="admin-user-detail-head hoard-panel hoard-panel-gradient">
|
||||
<div class="admin-user-detail-head__copy">
|
||||
<p class="hoard-kicker hoard-kicker--wide">Adminbereich</p>
|
||||
<h1>Benutzerdetails</h1>
|
||||
<p>Read-only Ansicht des ausgewählten Kontos.</p>
|
||||
</header>
|
||||
<p>Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.</p>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen...</p>
|
||||
|
||||
<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>
|
||||
<dd>{{ user.id }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Benutzername</dt>
|
||||
<dd>{{ user.userName || '(ohne Benutzername)' }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Rollen</dt>
|
||||
<dd>{{ rolesText }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Aktiv</dt>
|
||||
<dd>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Passwortwechsel</dt>
|
||||
<dd>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
|
||||
]"
|
||||
>
|
||||
{{ user.mustChangePassword ? 'Erforderlich' : 'Nicht erforderlich' }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div class="admin-user-detail-actions hoard-action-row">
|
||||
<div class="admin-user-detail-head__actions hoard-action-row">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
@@ -155,7 +112,7 @@ onMounted(() => {
|
||||
Zurück zur Liste
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@@ -164,126 +121,258 @@ onMounted(() => {
|
||||
Neu laden
|
||||
</v-btn>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<v-alert v-if="errorMessage" type="error" density="comfortable">
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<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">
|
||||
<header class="admin-user-detail-card__head">
|
||||
<span class="admin-user-detail-avatar">{{ userInitials }}</span>
|
||||
<div>
|
||||
<p class="hoard-kicker hoard-kicker--xs">Konto</p>
|
||||
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
|
||||
<p class="admin-user-detail-card__id">{{ user.id }}</p>
|
||||
</div>
|
||||
<div class="admin-user-detail-card__pills">
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
|
||||
]"
|
||||
>
|
||||
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'hoard-status',
|
||||
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
|
||||
]"
|
||||
>
|
||||
{{ user.mustChangePassword ? 'Passwort wechseln' : 'Passwort aktuell' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<hr class="hoard-divider-soft" />
|
||||
|
||||
<dl class="admin-user-detail-grid">
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>ID</dt>
|
||||
<dd class="admin-user-detail-item__mono">{{ user.id }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Benutzername</dt>
|
||||
<dd>{{ user.userName || '(ohne Benutzername)' }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Rollen</dt>
|
||||
<dd>{{ rolesText }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Konto aktiv</dt>
|
||||
<dd>{{ user.isActive ? 'Ja' : 'Nein' }}</dd>
|
||||
</div>
|
||||
<div class="admin-user-detail-item">
|
||||
<dt>Passwortwechsel</dt>
|
||||
<dd>{{ user.mustChangePassword ? 'Erforderlich' : 'Nicht erforderlich' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-user-detail-page {
|
||||
--hoard-page-width: 980px;
|
||||
--hoard-page-width: 1080px;
|
||||
}
|
||||
|
||||
.admin-user-detail-shell {
|
||||
.admin-user-detail-head {
|
||||
--hoard-gradient-angle: 120deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
grid-template-columns: minmax(0, 1.1fr) auto;
|
||||
gap: var(--space-5);
|
||||
align-items: end;
|
||||
padding: var(--space-7) var(--space-8);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.admin-user-detail-head__copy h1 {
|
||||
margin: 0 0 var(--space-2);
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.admin-user-detail-head__copy p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.admin-user-detail-head h1,
|
||||
.admin-user-detail-head p,
|
||||
.admin-user-detail-loading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-user-detail-head h1 {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.admin-user-detail-head p,
|
||||
.admin-user-detail-loading {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.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%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.admin-user-detail-profile {
|
||||
.admin-user-detail-card__head {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: var(--space-4);
|
||||
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%);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow:
|
||||
var(--shadow-sm),
|
||||
inset 0 0 0 2px color-mix(in srgb, var(--color-accent-lime) 22%, transparent);
|
||||
}
|
||||
|
||||
.admin-user-detail-profile-label {
|
||||
.admin-user-detail-card__head h2 {
|
||||
margin: var(--space-1) 0;
|
||||
font-size: var(--font-size-xl);
|
||||
letter-spacing: -0.01em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-user-detail-card__id {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.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-card__pills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.admin-user-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-4) var(--space-6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-user-detail-item {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.admin-user-detail-item:nth-last-child(-n + 2) {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-user-detail-item dt {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-user-detail-item dd {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.admin-user-detail-actions {
|
||||
justify-content: flex-end;
|
||||
.admin-user-detail-item__mono {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-sm) !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.admin-user-detail-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
.admin-user-detail-head,
|
||||
.admin-user-detail-card {
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
|
||||
.admin-user-detail-card {
|
||||
animation-delay: 80ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.admin-user-detail-head {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: flex-start;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.admin-user-detail-head__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-user-detail-card__head {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.admin-user-detail-card__pills {
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.admin-user-detail-shell {
|
||||
padding: var(--space-4);
|
||||
.admin-user-detail-head {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.admin-user-detail-card {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.admin-user-detail-card__head {
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-user-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-user-detail-actions {
|
||||
justify-content: stretch;
|
||||
.admin-user-detail-item:nth-last-child(-n + 2) {
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
padding-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-user-detail-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+440
-199
@@ -11,12 +11,59 @@ const appBannersStore = useAppBannersStore()
|
||||
const isLoading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const users = ref<AdminUser[]>([])
|
||||
const searchQuery = ref('')
|
||||
|
||||
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,
|
||||
)
|
||||
const adminCount = computed(
|
||||
() =>
|
||||
users.value.filter((user) =>
|
||||
user.roles.some((role) => role.toLowerCase() === 'admin'),
|
||||
).length,
|
||||
)
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase()
|
||||
if (query.length === 0) {
|
||||
return users.value
|
||||
}
|
||||
|
||||
return users.value.filter((user) => {
|
||||
if (user.userName.toLowerCase().includes(query)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (user.id.toLowerCase().includes(query)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return user.roles.some((role) => role.toLowerCase().includes(query))
|
||||
})
|
||||
})
|
||||
|
||||
function userInitials(user: AdminUser) {
|
||||
const name = user.userName?.trim() ?? ''
|
||||
if (name.length === 0) {
|
||||
return '·'
|
||||
}
|
||||
|
||||
const parts = name.split(/[\s._-]+/).filter((part) => part.length > 0)
|
||||
if (parts.length === 0) {
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
const first = parts[0] as string
|
||||
return first.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
const first = parts[0] as string
|
||||
const second = parts[1] as string
|
||||
return `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
|
||||
}
|
||||
|
||||
function formatRoles(roles: string[]): string {
|
||||
return roles.length > 0 ? roles.join(', ') : 'Keine Rolle'
|
||||
@@ -65,155 +112,26 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="admin-users-page hoard-page">
|
||||
<section class="admin-users-shell hoard-panel">
|
||||
<header class="admin-users-head">
|
||||
<p class="hoard-kicker">Adminbereich</p>
|
||||
<header class="admin-users-header hoard-panel hoard-panel-gradient">
|
||||
<div class="admin-users-header__copy">
|
||||
<p class="hoard-kicker hoard-kicker--wide">Adminbereich</p>
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
<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"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<p v-else-if="isLoading" class="admin-users-loading">Benutzer werden geladen...</p>
|
||||
|
||||
<section v-else-if="!hasUsers" class="hoard-empty-state">
|
||||
<h2>Keine Benutzer gefunden</h2>
|
||||
<p>Aktuell sind keine Konten vorhanden.</p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<p>Alle Hoard-Konten mit Rollen, Status und Passwortwechselpflicht – read-only.</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-users-actions hoard-action-row">
|
||||
<v-btn
|
||||
<div class="admin-users-header__actions hoard-action-row">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Benutzer, Rollen oder ID suchen"
|
||||
hide-details
|
||||
clearable
|
||||
class="admin-users-search"
|
||||
/>
|
||||
<v-btn
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@@ -222,76 +140,315 @@ onMounted(() => {
|
||||
Neu laden
|
||||
</v-btn>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht">
|
||||
<article class="admin-users-stat">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-account-group-outline" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="admin-users-stat__label">Konten</p>
|
||||
<p class="admin-users-stat__value">{{ users.length }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="admin-users-stat">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-account-check-outline" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="admin-users-stat__label">Aktiv</p>
|
||||
<p class="admin-users-stat__value">{{ activeUserCount }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="admin-users-stat">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-shield-account-outline" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="admin-users-stat__label">Admins</p>
|
||||
<p class="admin-users-stat__value">{{ adminCount }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="admin-users-stat">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-lock-reset" size="20" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="admin-users-stat__label">Passwortwechsel</p>
|
||||
<p class="admin-users-stat__value">{{ passwordChangeCount }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<v-alert v-if="errorMessage" type="error">
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<p v-else-if="isLoading" class="admin-users-loading">Benutzer werden geladen …</p>
|
||||
|
||||
<section v-else-if="!hasUsers" class="hoard-panel hoard-empty-state">
|
||||
<span class="hoard-icon-tile hoard-icon-tile--lg">
|
||||
<v-icon icon="mdi-account-question-outline" size="22" />
|
||||
</span>
|
||||
<h2>Keine Benutzer gefunden</h2>
|
||||
<p>Aktuell sind keine Konten vorhanden. Ein neuer Account muss vom Admin manuell angelegt werden.</p>
|
||||
</section>
|
||||
|
||||
<section v-else class="admin-users-listing hoard-panel">
|
||||
<header class="admin-users-listing__head hoard-toolbar">
|
||||
<div>
|
||||
<p class="admin-users-listing__title">Benutzer</p>
|
||||
<p class="admin-users-listing__meta">{{ filteredUsers.length }} von {{ users.length }} angezeigt</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p v-if="filteredUsers.length === 0" class="admin-users-empty-search">
|
||||
Keine Treffer für „{{ searchQuery }}".
|
||||
</p>
|
||||
|
||||
<div v-else class="admin-users-table-wrap">
|
||||
<v-table class="admin-users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzer</th>
|
||||
<th>Rollen</th>
|
||||
<th>Aktiv</th>
|
||||
<th>Passwort</th>
|
||||
<th class="admin-users-col-actions">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in filteredUsers" :key="user.id">
|
||||
<td>
|
||||
<div class="admin-users-cell-user">
|
||||
<span class="admin-users-cell-user__avatar">{{ userInitials(user) }}</span>
|
||||
<div>
|
||||
<p class="admin-users-cell-user__name">{{ user.userName || '(ohne Benutzername)' }}</p>
|
||||
<p class="admin-users-cell-user__id">{{ user.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</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' : 'Aktuell' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="admin-users-col-actions">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-arrow-right"
|
||||
@click="openUserDetail(user.id)"
|
||||
>
|
||||
Details
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredUsers.length > 0" class="admin-users-mobile-list" aria-label="Benutzerliste">
|
||||
<article v-for="user in filteredUsers" :key="user.id" class="admin-users-mobile-card">
|
||||
<header class="admin-users-mobile-head">
|
||||
<span class="admin-users-mobile-avatar">{{ userInitials(user) }}</span>
|
||||
<div>
|
||||
<p class="admin-users-mobile-label">Benutzer</p>
|
||||
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
|
||||
<p class="admin-users-mobile-id">{{ user.id }}</p>
|
||||
</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-arrow-right"
|
||||
block
|
||||
@click="openUserDetail(user.id)"
|
||||
>
|
||||
Details öffnen
|
||||
</v-btn>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-users-page {
|
||||
--hoard-page-width: 1120px;
|
||||
--hoard-page-width: 1200px;
|
||||
}
|
||||
|
||||
.admin-users-shell {
|
||||
/* ---------- Header ---------- */
|
||||
.admin-users-header {
|
||||
--hoard-gradient-angle: 120deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||
gap: var(--space-6);
|
||||
align-items: end;
|
||||
padding: var(--space-7) var(--space-8);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.admin-users-head h1,
|
||||
.admin-users-head p,
|
||||
.admin-users-loading {
|
||||
.admin-users-header__copy h1 {
|
||||
margin: 0 0 var(--space-2);
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.admin-users-header__copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-head h1 {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.admin-users-head p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.admin-users-loading {
|
||||
color: var(--color-text-secondary);
|
||||
.admin-users-header__actions {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-users-search {
|
||||
flex: 1 1 280px;
|
||||
min-width: 220px;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
/* ---------- Stats ---------- */
|
||||
.admin-users-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-users-stat {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
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%);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface) 95%, var(--color-surface-alt) 5%),
|
||||
var(--color-surface)
|
||||
);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.admin-users-stat-label,
|
||||
.admin-users-stat-value {
|
||||
.admin-users-stat__label,
|
||||
.admin-users-stat__value {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-stat-label {
|
||||
.admin-users-stat__label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-stat-value {
|
||||
.admin-users-stat__value {
|
||||
color: var(--color-text);
|
||||
font-size: 1.75rem;
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ---------- Listing ---------- */
|
||||
.admin-users-listing {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-users-listing__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-users-listing__title {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-users-listing__meta {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.admin-users-empty-search {
|
||||
margin: 0;
|
||||
padding: var(--space-6);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-users-loading {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.admin-users-table-wrap {
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.admin-users-mobile-list {
|
||||
@@ -299,12 +456,48 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.admin-users-table {
|
||||
min-width: 900px;
|
||||
min-width: 920px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.admin-users-cell-user {
|
||||
font-weight: 600;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-users-cell-user__avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.admin-users-cell-user__name,
|
||||
.admin-users-cell-user__id {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-cell-user__name {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-users-cell-user__id {
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-2xs);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-users-col-actions {
|
||||
@@ -312,26 +505,65 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-users-actions {
|
||||
justify-content: flex-end;
|
||||
.hoard-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.admin-users-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
.admin-users-header,
|
||||
.admin-users-stat,
|
||||
.admin-users-listing {
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
|
||||
.admin-users-stat:nth-child(2) { animation-delay: 60ms; }
|
||||
.admin-users-stat:nth-child(3) { animation-delay: 120ms; }
|
||||
.admin-users-stat:nth-child(4) { animation-delay: 180ms; }
|
||||
.admin-users-listing { animation-delay: 220ms; }
|
||||
}
|
||||
|
||||
@media (width <= 1100px) {
|
||||
.admin-users-header {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.admin-users-header__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-users-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.admin-users-header {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.admin-users-shell {
|
||||
padding: var(--space-4);
|
||||
overflow: hidden;
|
||||
.admin-users-header {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.admin-users-search {
|
||||
flex: 1 1 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.admin-users-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-users-listing {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-users-table-wrap {
|
||||
display: none;
|
||||
}
|
||||
@@ -339,6 +571,7 @@ onMounted(() => {
|
||||
.admin-users-mobile-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.admin-users-mobile-card {
|
||||
@@ -346,9 +579,10 @@ onMounted(() => {
|
||||
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: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 58%, var(--color-surface) 42%);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.admin-users-mobile-head {
|
||||
@@ -363,30 +597,41 @@ onMounted(() => {
|
||||
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%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-users-mobile-label,
|
||||
.admin-users-mobile-head h2,
|
||||
.admin-users-mobile-id,
|
||||
.admin-users-mobile-details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-users-mobile-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-users-mobile-head h2 {
|
||||
color: var(--color-text);
|
||||
font-size: 1.1rem;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-users-mobile-id {
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-2xs);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -399,7 +644,7 @@ onMounted(() => {
|
||||
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%);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.admin-users-mobile-details div:last-child {
|
||||
@@ -420,9 +665,5 @@ onMounted(() => {
|
||||
color: var(--color-text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-users-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,33 @@ const submitDisabled = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const passwordsMatch = computed(() => {
|
||||
if (newPassword.value.length === 0 || newPasswordConfirm.value.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return newPassword.value === newPasswordConfirm.value
|
||||
})
|
||||
|
||||
const passwordHints = computed(() => [
|
||||
{
|
||||
label: 'Mindestens 8 Zeichen',
|
||||
valid: newPassword.value.length >= 8,
|
||||
},
|
||||
{
|
||||
label: 'Buchstabe enthalten',
|
||||
valid: /[A-Za-zÄÖÜäöüß]/.test(newPassword.value),
|
||||
},
|
||||
{
|
||||
label: 'Ziffer enthalten',
|
||||
valid: /\d/.test(newPassword.value),
|
||||
},
|
||||
{
|
||||
label: 'Übereinstimmung mit Bestätigung',
|
||||
valid: passwordsMatch.value === true,
|
||||
},
|
||||
])
|
||||
|
||||
async function handleSubmit() {
|
||||
if (submitDisabled.value) {
|
||||
errorMessage.value = 'Bitte alle Passwortfelder ausfüllen.'
|
||||
@@ -74,70 +101,94 @@ async function handleSubmit() {
|
||||
<v-container fluid class="change-password-page hoard-page hoard-page--centered">
|
||||
<section class="change-password-shell hoard-panel hoard-shell-grid">
|
||||
<header class="change-password-head">
|
||||
<p class="hoard-kicker">Sicherheitsvorgabe</p>
|
||||
<h1>Passwort ändern</h1>
|
||||
<p>Hier kannst du ganz bequem dein Passwort aktualisieren.</p>
|
||||
<span class="hoard-icon-tile hoard-icon-tile--lg">
|
||||
<v-icon icon="mdi-shield-key-outline" size="24" />
|
||||
</span>
|
||||
<div>
|
||||
<p class="hoard-kicker hoard-kicker--xs">Sicherheitsvorgabe</p>
|
||||
<h1>Passwort ändern</h1>
|
||||
<p>Aktualisiere dein Hoard-Passwort. Nach der Änderung wirst du erneut zur Anmeldung weitergeleitet.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
density="comfortable"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<v-form class="change-password-form" @submit.prevent="handleSubmit">
|
||||
<v-text-field
|
||||
v-model="oldPassword"
|
||||
label="Altes Passwort"
|
||||
:type="showOldPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
:append-inner-icon="showOldPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showOldPassword = !showOldPassword"
|
||||
/>
|
||||
<section class="change-password-section">
|
||||
<p class="hoard-kicker hoard-kicker--xs hoard-kicker--plain">Aktuell</p>
|
||||
<v-text-field
|
||||
v-model="oldPassword"
|
||||
label="Altes Passwort"
|
||||
:type="showOldPassword ? 'text' : 'password'"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
:append-inner-icon="showOldPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showOldPassword = !showOldPassword"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<v-divider class="change-password-divider" />
|
||||
<p class="change-password-section-label">Neues Passwort</p>
|
||||
<hr class="hoard-divider-soft" />
|
||||
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
label="Neues Passwort"
|
||||
:type="showNewPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-reset"
|
||||
:append-inner-icon="showNewPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showNewPassword = !showNewPassword"
|
||||
/>
|
||||
<section class="change-password-section">
|
||||
<p class="hoard-kicker hoard-kicker--xs hoard-kicker--plain">Neues Passwort</p>
|
||||
|
||||
<v-text-field
|
||||
v-model="newPasswordConfirm"
|
||||
label="Neues Passwort bestätigen"
|
||||
:type="showNewPasswordConfirm ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-check-outline"
|
||||
:append-inner-icon="showNewPasswordConfirm ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showNewPasswordConfirm = !showNewPasswordConfirm"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
label="Neues Passwort"
|
||||
:type="showNewPassword ? 'text' : 'password'"
|
||||
prepend-inner-icon="mdi-lock-reset"
|
||||
:append-inner-icon="showNewPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showNewPassword = !showNewPassword"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="newPasswordConfirm"
|
||||
label="Neues Passwort bestätigen"
|
||||
:type="showNewPasswordConfirm ? 'text' : 'password'"
|
||||
prepend-inner-icon="mdi-lock-check-outline"
|
||||
:append-inner-icon="showNewPasswordConfirm ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
:error="passwordsMatch === false"
|
||||
:error-messages="
|
||||
passwordsMatch === false ? 'Bestätigung stimmt nicht mit dem neuen Passwort überein.' : undefined
|
||||
"
|
||||
@click:append-inner="showNewPasswordConfirm = !showNewPasswordConfirm"
|
||||
/>
|
||||
|
||||
<ul class="password-hints">
|
||||
<li
|
||||
v-for="hint in passwordHints"
|
||||
:key="hint.label"
|
||||
:class="['password-hint', { 'password-hint--valid': hint.valid }]"
|
||||
>
|
||||
<v-icon :icon="hint.valid ? 'mdi-check-circle' : 'mdi-circle-outline'" size="14" />
|
||||
{{ hint.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p class="change-password-hint">
|
||||
Nach erfolgreicher Änderung wirst du automatisch abgemeldet und meldest dich mit dem neuen Passwort wieder an.
|
||||
<v-icon icon="mdi-information-outline" size="14" />
|
||||
Nach erfolgreicher Änderung wirst du automatisch abgemeldet und meldest dich anschließend mit deinem
|
||||
neuen Passwort wieder an.
|
||||
</p>
|
||||
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
prepend-icon="mdi-content-save-outline"
|
||||
:loading="isSubmitting"
|
||||
@@ -157,58 +208,95 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.change-password-shell {
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.change-password-head h1,
|
||||
.change-password-head p {
|
||||
margin: 0;
|
||||
.change-password-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.change-password-head h1 {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
margin: var(--space-1) 0 var(--space-2);
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.change-password-head p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.change-password-form {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.change-password-section {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.change-password-divider {
|
||||
margin-top: var(--space-1);
|
||||
margin-bottom: 0;
|
||||
border-color: color-mix(in srgb, var(--color-border) 82%, var(--color-surface) 18%);
|
||||
.password-hints {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-2);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.change-password-section-label {
|
||||
margin: 0;
|
||||
color: var(--color-primary-700);
|
||||
.password-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.3;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.password-hint--valid {
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.password-hint .v-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.change-password-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 80%, var(--color-surface) 20%);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.change-password-hint .v-icon {
|
||||
color: var(--color-primary-700);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.change-password-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.change-password-form {
|
||||
gap: var(--space-2);
|
||||
.change-password-head {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.password-hints {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import iconImage from '@/assets/images/icon.svg'
|
||||
import { AuthRequestError, login } from '@/services/authSession'
|
||||
import { useAppBannersStore } from '@/stores/appBanners'
|
||||
|
||||
@@ -79,104 +81,186 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="login-page hoard-page hoard-page--centered">
|
||||
<section class="login-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
|
||||
<section class="login-shell hoard-panel hoard-panel-gradient hoard-spotlight">
|
||||
<aside class="login-brand">
|
||||
<p class="login-kicker hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p>
|
||||
<h1>Anmelden und weiterarbeiten</h1>
|
||||
<div class="login-brand__logo">
|
||||
<span class="login-brand__halo" aria-hidden="true" />
|
||||
<img :src="iconImage" alt="Hoard Icon" />
|
||||
</div>
|
||||
|
||||
<p class="hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p>
|
||||
<h1>
|
||||
Deine Dateien.<br />
|
||||
<span class="login-brand__accent">Aufgeräumt.</span>
|
||||
</h1>
|
||||
<p class="login-intro">
|
||||
Deine Dateiablage bleibt aufgeräumt, schnell und direkt im Browser bedienbar.
|
||||
Hoard ist deine ruhige, self-hosted Dateiablage – schnell, übersichtlich und direkt im Browser.
|
||||
Melde dich an und mach weiter, wo du aufgehört hast.
|
||||
</p>
|
||||
|
||||
<ul class="login-points">
|
||||
<li>
|
||||
<v-icon icon="mdi-folder-outline" size="18" />
|
||||
Ordner und Dateien zentral verwalten
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-folder-outline" size="18" /></span>
|
||||
<div>
|
||||
<p class="login-points__title">Ordner und Dateien</p>
|
||||
<p class="login-points__text">Zentral organisieren, schnell finden, sauber strukturieren.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<v-icon icon="mdi-file-document-edit-outline" size="18" />
|
||||
Markdown-Dateien sofort bearbeiten
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-language-markdown-outline" size="18" /></span>
|
||||
<div>
|
||||
<p class="login-points__title">Markdown direkt im Browser</p>
|
||||
<p class="login-points__text">Notizen lesen und bearbeiten, ohne externes Tool.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<v-icon icon="mdi-image-outline" size="18" />
|
||||
Bilder und PDFs direkt als Vorschau ansehen
|
||||
<span class="hoard-icon-tile"><v-icon icon="mdi-shield-check-outline" size="18" /></span>
|
||||
<div>
|
||||
<p class="login-points__title">Self-hosted & sicher</p>
|
||||
<p class="login-points__text">Cookie-Auth, Rollenmodell, deine Infrastruktur.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<v-form class="login-form" @submit.prevent="handleSubmit">
|
||||
<div class="form-head">
|
||||
<h2>Login</h2>
|
||||
<p>Melde dich mit deinem bestehenden Konto an.</p>
|
||||
<header class="login-form__head">
|
||||
<p class="hoard-kicker hoard-kicker--xs">Login</p>
|
||||
<h2>Anmelden</h2>
|
||||
<p>Melde dich mit deinem bestehenden Hoard-Konto an.</p>
|
||||
</header>
|
||||
|
||||
<div class="login-form__fields">
|
||||
<v-text-field
|
||||
v-model="userName"
|
||||
label="Benutzername"
|
||||
type="text"
|
||||
prepend-inner-icon="mdi-account-outline"
|
||||
autocomplete="username"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Passwort"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="userName"
|
||||
label="Benutzername"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-account-outline"
|
||||
autocomplete="username"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Passwort"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
:disabled="isSubmitting"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
|
||||
<div class="form-meta">
|
||||
<p>Anmeldung erfolgt per sicherem Session-Cookie.</p>
|
||||
</div>
|
||||
<p class="login-form__hint">
|
||||
<v-icon icon="mdi-cookie-outline" size="14" />
|
||||
Anmeldung erfolgt per sicherem Session-Cookie. Keine Tokens, keine Drittanbieter.
|
||||
</p>
|
||||
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
block
|
||||
size="large"
|
||||
prepend-icon="mdi-login"
|
||||
prepend-icon="mdi-arrow-right"
|
||||
:loading="isSubmitting"
|
||||
:disabled="submitDisabled"
|
||||
>
|
||||
Anmelden
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="outlined" block to="/welcome" prepend-icon="mdi-home">Zur Startseite</v-btn>
|
||||
<v-btn variant="text" block to="/welcome" prepend-icon="mdi-home-outline" size="small">
|
||||
Zurück zur Startseite
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-shell {
|
||||
--hoard-shell-width: 1040px;
|
||||
--hoard-gradient-angle: 115deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface) 55%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 52%;
|
||||
.login-page {
|
||||
--hoard-centered-offset: 200px;
|
||||
}
|
||||
|
||||
grid-template-columns: minmax(280px, 1fr) minmax(320px, 430px);
|
||||
.login-shell {
|
||||
--hoard-shell-width: 1080px;
|
||||
--hoard-shell-padding: 0;
|
||||
--hoard-shell-gap: 0;
|
||||
--hoard-gradient-angle: 120deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 60%;
|
||||
|
||||
grid-template-columns: minmax(280px, 1.1fr) minmax(320px, 460px);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
position: relative;
|
||||
padding: var(--space-10) var(--space-8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-brand__logo {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.login-brand__halo {
|
||||
position: absolute;
|
||||
inset: -12px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
radial-gradient(
|
||||
closest-side,
|
||||
color-mix(in srgb, var(--color-accent-lime) 38%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
filter: blur(10px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.login-brand__logo img {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 8px 18px rgb(28 101 47 / 30%));
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: var(--space-3);
|
||||
max-width: 18ch;
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 var(--space-3);
|
||||
max-width: 16ch;
|
||||
font-size: clamp(2rem, 1.4rem + 1.5vw, 2.8rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.login-brand__accent {
|
||||
background: linear-gradient(120deg, var(--color-primary-700), var(--color-primary-500));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.login-intro {
|
||||
margin-bottom: var(--space-5);
|
||||
max-width: 44ch;
|
||||
margin: 0 0 var(--space-6);
|
||||
max-width: 46ch;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.login-points {
|
||||
@@ -189,65 +273,90 @@ h1 {
|
||||
}
|
||||
|
||||
.login-points li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.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-points__title,
|
||||
.login-points__text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-points__title {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.login-points__text {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
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);
|
||||
padding: var(--space-8);
|
||||
border-left: 1px solid var(--color-border);
|
||||
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);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.form-head h2 {
|
||||
margin-bottom: var(--space-1);
|
||||
font-size: 1.45rem;
|
||||
.login-form__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.form-head p {
|
||||
.login-form__head h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.login-form__head p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.form-meta {
|
||||
display: flex;
|
||||
.login-form__fields {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.login-form__hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 80%, var(--color-surface) 20%);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.form-meta p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
.login-form__hint .v-icon {
|
||||
color: var(--color-primary-700);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.login-brand,
|
||||
.login-form {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
animation: hoard-soft-enter 320ms both;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
@@ -260,44 +369,34 @@ h1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: var(--space-5);
|
||||
.login-brand {
|
||||
padding: var(--space-7) var(--space-6) 0;
|
||||
}
|
||||
|
||||
.form-meta {
|
||||
flex-wrap: wrap;
|
||||
.login-form {
|
||||
padding: var(--space-6);
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
h1 {
|
||||
max-width: none;
|
||||
font-size: 1.85rem;
|
||||
.login-brand {
|
||||
padding: var(--space-6) var(--space-5) 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.login-intro {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.login-points {
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.login-points li {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.form-meta {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
:deep(.login-form .v-btn) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { AuthRequestError, fetchCurrentUser, type CurrentUser } from '@/services/authSession'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -15,6 +16,28 @@ const prettyUser = computed(() => {
|
||||
|
||||
return JSON.stringify(user.value, null, 2)
|
||||
})
|
||||
|
||||
const userInitials = computed(() => {
|
||||
const name = user.value?.userName?.trim() ?? ''
|
||||
if (name.length === 0) {
|
||||
return 'H'
|
||||
}
|
||||
|
||||
const parts = name.split(/[\s._-]+/).filter((part) => part.length > 0)
|
||||
if (parts.length === 0) {
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
const first = parts[0] as string
|
||||
return first.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
const first = parts[0] as string
|
||||
const second = parts[1] as string
|
||||
return `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
|
||||
})
|
||||
|
||||
const roleLabel = computed(() => {
|
||||
if (!user.value || user.value.roles.length === 0) {
|
||||
return 'Keine Rolle'
|
||||
@@ -22,9 +45,20 @@ const roleLabel = computed(() => {
|
||||
|
||||
return user.value.roles.join(', ')
|
||||
})
|
||||
const accountStateLabel = computed(() => (user.value?.isActive ? 'Aktiv' : 'Inaktiv'))
|
||||
const passwordStateLabel = computed(() =>
|
||||
user.value?.mustChangePassword ? 'Erforderlich' : 'Aktuell',
|
||||
|
||||
const roleChips = computed(() => {
|
||||
if (!user.value || user.value.roles.length === 0) {
|
||||
return [] as string[]
|
||||
}
|
||||
|
||||
return user.value.roles
|
||||
})
|
||||
|
||||
const accountStatusVariant = computed(() => (user.value?.isActive ? 'success' : 'danger'))
|
||||
const accountStatusLabel = computed(() => (user.value?.isActive ? 'Aktiv' : 'Inaktiv'))
|
||||
const passwordStatusVariant = computed(() => (user.value?.mustChangePassword ? 'warning' : 'info'))
|
||||
const passwordStatusLabel = computed(() =>
|
||||
user.value?.mustChangePassword ? 'Wechsel erforderlich' : 'Aktuell',
|
||||
)
|
||||
|
||||
async function loadCurrentUser() {
|
||||
@@ -50,6 +84,10 @@ async function loadCurrentUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function goToChangePassword() {
|
||||
void router.push({ name: 'ChangePassword' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadCurrentUser()
|
||||
})
|
||||
@@ -57,247 +95,372 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-container fluid class="dashboard-page hoard-page">
|
||||
<section class="dashboard-shell hoard-panel">
|
||||
<header class="dashboard-head">
|
||||
<p class="hoard-kicker">Geschützter Bereich</p>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Hier siehst du aktuell direkt die Antwort von <code>/auth/me</code>.</p>
|
||||
</header>
|
||||
<header class="dashboard-greeting hoard-panel hoard-panel-gradient hoard-spotlight">
|
||||
<div class="dashboard-greeting__copy">
|
||||
<p class="hoard-kicker hoard-kicker--wide">Geschützter Bereich</p>
|
||||
<h1>
|
||||
<template v-if="user">Hallo, {{ user.userName || 'willkommen' }}.</template>
|
||||
<template v-else>Dashboard</template>
|
||||
</h1>
|
||||
<p>
|
||||
Hier siehst du den aktuellen Status deines Kontos. Sobald die Datei- und Markdown-Module live
|
||||
gehen, wird dieser Bereich dein Startpunkt in den Workspace.
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
border="start"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
<div class="dashboard-greeting__chips">
|
||||
<span v-for="role in roleChips" :key="role" class="hoard-chip hoard-chip--brand">
|
||||
<v-icon icon="mdi-shield-account-outline" size="14" />
|
||||
{{ role }}
|
||||
</span>
|
||||
<span v-if="roleChips.length === 0" class="hoard-chip">
|
||||
<v-icon icon="mdi-account-outline" size="14" /> Keine Rollen
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div v-if="user" class="dashboard-greeting__avatar">
|
||||
<span class="dashboard-avatar">{{ userInitials }}</span>
|
||||
<p class="dashboard-avatar__caption">{{ user.userName }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
density="comfortable"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<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>
|
||||
<section v-else class="dashboard-grid">
|
||||
<article class="dashboard-stat hoard-panel">
|
||||
<div class="dashboard-stat__head">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-account-outline" size="20" />
|
||||
</span>
|
||||
<p class="dashboard-stat__label">Konto</p>
|
||||
</div>
|
||||
<p class="dashboard-stat__value">{{ user?.userName || '—' }}</p>
|
||||
<p class="dashboard-stat__hint">Angemeldet als interner Benutzer</p>
|
||||
</article>
|
||||
|
||||
<div class="dashboard-actions">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="loadCurrentUser"
|
||||
>
|
||||
Neu laden
|
||||
</v-btn>
|
||||
</div>
|
||||
<article class="dashboard-stat hoard-panel">
|
||||
<div class="dashboard-stat__head">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-shield-account-outline" size="20" />
|
||||
</span>
|
||||
<p class="dashboard-stat__label">Rollen</p>
|
||||
</div>
|
||||
<p class="dashboard-stat__value">{{ roleLabel }}</p>
|
||||
<p class="dashboard-stat__hint">Definiert deine sichtbaren Bereiche</p>
|
||||
</article>
|
||||
|
||||
<article class="dashboard-stat hoard-panel">
|
||||
<div class="dashboard-stat__head">
|
||||
<span class="hoard-icon-tile">
|
||||
<v-icon icon="mdi-pulse" size="20" />
|
||||
</span>
|
||||
<p class="dashboard-stat__label">Status</p>
|
||||
</div>
|
||||
<div class="dashboard-stat__pill-row">
|
||||
<span :class="['hoard-status', `hoard-status--${accountStatusVariant}`]">
|
||||
{{ accountStatusLabel }}
|
||||
</span>
|
||||
<span :class="['hoard-status', `hoard-status--${passwordStatusVariant}`]">
|
||||
{{ passwordStatusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="dashboard-stat__hint">Konto- und Passwortzustand</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section v-if="!errorMessage" class="dashboard-detail hoard-panel">
|
||||
<header class="dashboard-detail__head">
|
||||
<div>
|
||||
<p class="hoard-kicker">Auth-Antwort</p>
|
||||
<h2>Aktueller Session-Snapshot</h2>
|
||||
<p>Direkt aus <code>GET /auth/me</code> – nützlich zum Debuggen und für Plausibilitätschecks.</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-detail__actions hoard-action-row">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-lock-reset"
|
||||
@click="goToChangePassword"
|
||||
>
|
||||
Passwort ändern
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="loadCurrentUser"
|
||||
>
|
||||
Neu laden
|
||||
</v-btn>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p v-if="isLoading" class="dashboard-detail__loading">Benutzerdaten werden geladen …</p>
|
||||
<pre v-else class="dashboard-detail__json">{{ prettyUser }}</pre>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
--hoard-page-width: 980px;
|
||||
--hoard-page-width: 1120px;
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
/* ---------- Greeting ---------- */
|
||||
.dashboard-greeting {
|
||||
--hoard-gradient-angle: 120deg;
|
||||
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
|
||||
--hoard-gradient-end: var(--color-surface);
|
||||
--hoard-gradient-end-stop: 65%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-6);
|
||||
align-items: center;
|
||||
padding: var(--space-8);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-greeting__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-size: clamp(1.8rem, 1.4rem + 1vw, 2.4rem);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.dashboard-greeting__copy p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
max-width: 64ch;
|
||||
}
|
||||
|
||||
.dashboard-greeting__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.dashboard-greeting__avatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dashboard-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: var(--radius-full);
|
||||
background:
|
||||
linear-gradient(135deg, var(--color-primary-600), var(--color-primary-800));
|
||||
color: var(--color-text-on-primary);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
box-shadow:
|
||||
var(--shadow-glow),
|
||||
inset 0 0 0 2px color-mix(in srgb, var(--color-accent-lime) 25%, transparent);
|
||||
}
|
||||
|
||||
.dashboard-avatar__caption {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---------- Stats grid ---------- */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.dashboard-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.dashboard-stat__head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.dashboard-stat__label {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-stat__value {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-stat__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.dashboard-stat__pill-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* ---------- Detail panel ---------- */
|
||||
.dashboard-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.dashboard-head h1,
|
||||
.dashboard-head p,
|
||||
.dashboard-label,
|
||||
.dashboard-loading {
|
||||
.dashboard-detail__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard-detail__head h2 {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-xl);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.dashboard-detail__head p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-head h1 {
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.dashboard-head p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text-secondary);
|
||||
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 {
|
||||
.dashboard-detail__loading {
|
||||
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);
|
||||
}
|
||||
|
||||
.dashboard-json {
|
||||
.dashboard-detail__json {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: color-mix(in srgb, var(--color-surface-alt) 70%, var(--color-surface) 30%);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-surface-alt);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.dashboard-shell {
|
||||
animation: hoard-soft-enter 260ms both;
|
||||
.dashboard-greeting,
|
||||
.dashboard-stat,
|
||||
.dashboard-detail {
|
||||
animation: hoard-soft-enter 280ms both;
|
||||
}
|
||||
|
||||
.dashboard-stat:nth-child(2) {
|
||||
animation-delay: 60ms;
|
||||
}
|
||||
|
||||
.dashboard-stat:nth-child(3) {
|
||||
animation-delay: 120ms;
|
||||
}
|
||||
|
||||
.dashboard-detail {
|
||||
animation-delay: 160ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.dashboard-greeting {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: left;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.dashboard-greeting__avatar {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-detail {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.dashboard-page,
|
||||
.dashboard-shell,
|
||||
.dashboard-user,
|
||||
.dashboard-summary-grid,
|
||||
.dashboard-summary-card {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
.dashboard-greeting {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
.dashboard-stat,
|
||||
.dashboard-detail {
|
||||
padding: var(--space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.dashboard-detail__head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dashboard-user {
|
||||
padding: var(--space-3);
|
||||
overflow: hidden;
|
||||
.dashboard-detail__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-summary-card {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
:deep(.dashboard-detail__actions .v-btn) {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.dashboard-head p,
|
||||
.dashboard-label {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dashboard-json {
|
||||
.dashboard-detail__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 {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
:deep(.dashboard-actions .v-btn) {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/* Shared layout primitives for route-level page shells */
|
||||
/* =============================================================================
|
||||
Hoard – Page-Layout-Primitives
|
||||
============================================================================= */
|
||||
|
||||
.hoard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--hoard-page-gap, var(--space-6));
|
||||
margin-inline: auto;
|
||||
width: min(100%, var(--hoard-page-width, 1120px));
|
||||
width: min(100%, var(--hoard-page-width, 1180px));
|
||||
padding-block:
|
||||
var(--hoard-page-padding-start, var(--space-4))
|
||||
var(--hoard-page-padding-end, var(--space-8));
|
||||
var(--hoard-page-padding-start, var(--space-5))
|
||||
var(--hoard-page-padding-end, var(--space-12));
|
||||
}
|
||||
|
||||
.hoard-page--centered {
|
||||
@@ -15,41 +18,76 @@
|
||||
margin-inline: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - var(--hoard-centered-offset, 210px));
|
||||
padding: var(--hoard-centered-padding, var(--space-8) var(--space-4));
|
||||
min-height: calc(100vh - var(--hoard-centered-offset, 220px));
|
||||
padding: var(--hoard-centered-padding, var(--space-10) var(--space-4));
|
||||
}
|
||||
|
||||
.hoard-shell-grid {
|
||||
display: grid;
|
||||
gap: var(--hoard-shell-gap, var(--space-8));
|
||||
width: min(100%, var(--hoard-shell-width, 1040px));
|
||||
width: min(100%, var(--hoard-shell-width, 1080px));
|
||||
padding: var(--hoard-shell-padding, var(--space-8));
|
||||
}
|
||||
|
||||
.hoard-page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
max-width: 78ch;
|
||||
}
|
||||
|
||||
.hoard-page-header > h1 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-3xl);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hoard-page-header > p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: var(--line-height-loose);
|
||||
}
|
||||
|
||||
.hoard-page-header__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.hoard-page {
|
||||
width: 100%;
|
||||
gap: var(--hoard-page-gap-mobile, var(--space-5));
|
||||
padding-inline:
|
||||
var(--hoard-page-padding-inline-start-mobile, max(var(--space-2), env(safe-area-inset-left)))
|
||||
var(--hoard-page-padding-inline-end-mobile, max(var(--space-2), env(safe-area-inset-right)));
|
||||
var(--hoard-page-padding-inline-start-mobile, max(var(--space-3), env(safe-area-inset-left)))
|
||||
var(--hoard-page-padding-inline-end-mobile, max(var(--space-3), env(safe-area-inset-right)));
|
||||
padding-block:
|
||||
var(--hoard-page-padding-start-mobile, var(--space-2))
|
||||
var(--hoard-page-padding-end-mobile, var(--space-6));
|
||||
var(--hoard-page-padding-start-mobile, var(--space-3))
|
||||
var(--hoard-page-padding-end-mobile, var(--space-8));
|
||||
}
|
||||
|
||||
.hoard-page--centered {
|
||||
width: 100%;
|
||||
min-height: calc(100vh - var(--hoard-centered-offset-mobile, 180px));
|
||||
padding: var(--hoard-centered-padding-mobile, var(--space-5) var(--space-2));
|
||||
min-height: calc(100vh - var(--hoard-centered-offset-mobile, 200px));
|
||||
padding: var(--hoard-centered-padding-mobile, var(--space-6) var(--space-3));
|
||||
}
|
||||
|
||||
.hoard-shell-grid {
|
||||
width: 100%;
|
||||
gap: var(--hoard-shell-gap-mobile, var(--space-5));
|
||||
padding:
|
||||
var(--hoard-shell-padding-block-mobile, var(--space-5))
|
||||
var(--hoard-shell-padding-inline-mobile, var(--space-4));
|
||||
var(--hoard-shell-padding-block-mobile, var(--space-6))
|
||||
var(--hoard-shell-padding-inline-mobile, var(--space-5));
|
||||
}
|
||||
|
||||
.hoard-page-header > h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.hoard-page-header > p {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +96,18 @@
|
||||
gap: var(--hoard-page-gap-mobile-xs, var(--space-4));
|
||||
padding-block:
|
||||
var(--hoard-page-padding-start-mobile-xs, var(--space-2))
|
||||
var(--hoard-page-padding-end-mobile-xs, var(--space-5));
|
||||
var(--hoard-page-padding-end-mobile-xs, var(--space-6));
|
||||
}
|
||||
|
||||
.hoard-page--centered {
|
||||
min-height: calc(100vh - var(--hoard-centered-offset-mobile-xs, 164px));
|
||||
padding: var(--hoard-centered-padding-mobile-xs, var(--space-4) var(--space-2));
|
||||
min-height: calc(100vh - var(--hoard-centered-offset-mobile-xs, 180px));
|
||||
padding: var(--hoard-centered-padding-mobile-xs, var(--space-5) var(--space-3));
|
||||
}
|
||||
|
||||
.hoard-shell-grid {
|
||||
gap: var(--hoard-shell-gap-mobile-xs, var(--space-4));
|
||||
padding:
|
||||
var(--hoard-shell-padding-block-mobile-xs, var(--space-4))
|
||||
var(--hoard-shell-padding-inline-mobile-xs, var(--space-3));
|
||||
var(--hoard-shell-padding-block-mobile-xs, var(--space-5))
|
||||
var(--hoard-shell-padding-inline-mobile-xs, var(--space-4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,219 @@
|
||||
/* Shared surface and content patterns */
|
||||
/* =============================================================================
|
||||
Hoard – Surface- und Inhaltspattern
|
||||
============================================================================= */
|
||||
|
||||
.hoard-kicker {
|
||||
margin: 0 0 var(--space-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--color-primary-700);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hoard-kicker::before {
|
||||
content: '';
|
||||
width: 18px;
|
||||
height: 1px;
|
||||
background-color: currentcolor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.hoard-kicker--wide {
|
||||
letter-spacing: 0.06em;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.hoard-kicker--xs {
|
||||
font-size: var(--font-size-xs);
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: var(--space-2);
|
||||
font-size: var(--font-size-2xs);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hoard-kicker--plain::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hoard-action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.hoard-action-row--end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.hoard-panel-gradient {
|
||||
background: linear-gradient(
|
||||
var(--hoard-gradient-angle, 120deg),
|
||||
var(--hoard-gradient-start, color-mix(in srgb, var(--color-primary-100) 34%, var(--color-surface) 66%))
|
||||
0%,
|
||||
var(--hoard-gradient-end, var(--color-surface)) var(--hoard-gradient-end-stop, 52%)
|
||||
);
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 80% at 100% 0%,
|
||||
color-mix(in srgb, var(--color-accent-lime) 14%, transparent) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
linear-gradient(
|
||||
var(--hoard-gradient-angle, 130deg),
|
||||
var(--hoard-gradient-start, color-mix(in srgb, var(--color-primary-100) 65%, var(--color-surface) 35%))
|
||||
0%,
|
||||
var(--hoard-gradient-end, var(--color-surface)) var(--hoard-gradient-end-stop, 60%)
|
||||
);
|
||||
}
|
||||
|
||||
.hoard-spotlight {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.hoard-spotlight::before,
|
||||
.hoard-spotlight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
border-radius: var(--radius-full);
|
||||
filter: blur(60px);
|
||||
}
|
||||
|
||||
.hoard-spotlight::before {
|
||||
inset: -10% auto auto -10%;
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background: color-mix(in srgb, var(--color-primary-300) 40%, transparent);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.hoard-spotlight::after {
|
||||
inset: auto -10% -10% auto;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: color-mix(in srgb, var(--color-accent-lime) 35%, transparent);
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .hoard-spotlight::before {
|
||||
background: color-mix(in srgb, var(--color-primary-700) 50%, transparent);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .hoard-spotlight::after {
|
||||
background: color-mix(in srgb, var(--color-primary-600) 30%, transparent);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.hoard-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 5px var(--space-3);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: color-mix(in srgb, var(--color-surface) 88%, var(--color-surface-alt) 12%);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hoard-chip--brand {
|
||||
border-color: color-mix(in srgb, var(--color-primary-300) 60%, var(--color-border) 40%);
|
||||
background-color: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.hoard-chip--ghost {
|
||||
border-color: var(--color-border-subtle);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.hoard-chip > .v-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.hoard-icon-tile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-primary-700);
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-primary-100) 90%, var(--color-surface) 10%),
|
||||
color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%)
|
||||
);
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary-300) 35%, var(--color-border) 65%);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.hoard-icon-tile--lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.hoard-icon-tile--ghost {
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.hoard-divider-soft {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--color-border-subtle) 12%,
|
||||
var(--color-border) 50%,
|
||||
var(--color-border-subtle) 88%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.hoard-section-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-5);
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.hoard-section-head > h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-2xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.hoard-section-head > p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.hoard-action-row {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.hoard-spotlight::before,
|
||||
.hoard-spotlight::after {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
filter: blur(48px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.hoard-kicker {
|
||||
margin-bottom: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.hoard-action-row {
|
||||
@@ -52,4 +225,8 @@
|
||||
.hoard-action-row > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hoard-section-head > h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
+270
-383
@@ -1,471 +1,358 @@
|
||||
# Hoard – Style Guide
|
||||
|
||||
## Zielbild
|
||||
Hoard soll wirken wie eine ruhige, moderne Dateiverwaltung im Browser: klar, aufgeräumt, produktiv und leicht verständlich. Nicht verspielt, nicht luxuriös, nicht wie ein komplexes Notion-Klon-System. Die Oberfläche soll in erster Linie Ordnung vermitteln und den Fokus auf Dateien, Ordner, Vorschau und Markdown-Bearbeitung legen.
|
||||
Hoard soll wirken wie eine **moderne, produktive Dateiverwaltung im Browser** – ruhig, klar und mit hoher visueller Sorgfalt. Die Grundphilosophie bleibt dateiorientiert: Inhalte, Dateinamen, Pfade und Aktionen stehen im Vordergrund, nicht UI-Effekte. Gleichzeitig darf sich Hoard hochwertig und präzise anfühlen – „polished" statt „fancy", näher an einer modernen Linear-/Vercel-/Notion-Sidebar als an einer alten Admin-UI.
|
||||
|
||||
Die Gestaltung orientiert sich an drei Prinzipien:
|
||||
Die Gestaltung folgt drei Prinzipien:
|
||||
|
||||
1. **Dateien zuerst** – Inhalte, Dateinamen, Pfade und Aktionen stehen optisch im Vordergrund.
|
||||
2. **Ruhige Oberfläche** – wenig visuelle Unruhe, viel Weißraum, zurückhaltende Farben.
|
||||
3. **Grün als Identität, nicht als Dauerfeuer** – die Markenfarbe wird gezielt für Auswahl, Primäraktionen und Status genutzt, nicht flächendeckend.
|
||||
1. **Dateien zuerst** – Inhalte, Namen, Strukturen und Aktionen sind optisch dominant, nicht die Chrome.
|
||||
2. **Ruhe mit Charakter** – viel Weißraum, klare Flächen, ausgewählte Akzente. Wenig Elemente, aber jedes mit Sorgfalt.
|
||||
3. **Grün als Identität, nicht als Dauerfeuer** – die Markenfarbe wird gezielt für Auswahl, Primäraktionen, Status und Branding eingesetzt – nicht flächendeckend.
|
||||
|
||||
## Stilrichtung
|
||||
Die Seite soll sich optisch zwischen Google Drive und einer modernen self-hosted Admin-Oberfläche bewegen.
|
||||
## Modernisierungs-Direktive
|
||||
Beim Redesign gilt zusätzlich:
|
||||
|
||||
**So soll es wirken:**
|
||||
- sachlich und sauber
|
||||
- freundlich, aber nicht verspielt
|
||||
- modern, aber bewusst einfach
|
||||
- produktiv statt marketing-lastig
|
||||
- leicht technisch, ohne kalt zu sein
|
||||
- **Hochwertige App-Shell:** Sidebar und Topbar dürfen sich „premium" anfühlen (klare Hierarchie, animierter Active-Indicator, gradient-tinged Brand-Bereich).
|
||||
- **Layered Surfaces:** Statt einer einzigen Surface-Farbe gibt es eine kleine Surface-Hierarchie (`surface`, `surface-alt`, `surface-elevated`) mit subtil gestaffelten Schatten.
|
||||
- **Microinteractions:** kurze, präzise Hover-/Active-/Focus-Animationen (160–240 ms). Keine wabernden, dauerhaften Bewegungen.
|
||||
- **Ambient Gradients:** Hero-/Brand-Bereiche dürfen sehr weiche, breit gestreute Verläufe in Markenfarbe haben. Arbeitsflächen bleiben funktional und ruhig.
|
||||
- **Typografische Präzision:** klare Größen-Hierarchie, ruhige Headlines, viel `letter-spacing` Disziplin, keine dekorativen Schriften.
|
||||
- **Konsistente Iconographie:** ausschließlich MDI Outline-Icons mit gleicher Größe und Farbverhalten.
|
||||
- **Light- und Dark-Mode gleichwertig:** alle neuen Stile müssen in beiden Modi funktional und ästhetisch tragen, ausschließlich über Tokens (`color-mix`, CSS-Variablen) gelöst.
|
||||
|
||||
**So soll es nicht wirken:**
|
||||
- kein Neon- oder Gaming-Look
|
||||
- kein Glassmorphism
|
||||
- keine harten Kontraste überall
|
||||
- keine überladenen Kartenlayouts
|
||||
- keine bunte Mischung vieler Akzentfarben
|
||||
**Was weiterhin tabu ist:**
|
||||
- kein Glassmorphism (echte Backdrop-Blur-Karten),
|
||||
- kein Neon-/Gaming-Look,
|
||||
- keine harten, durchgängigen Kontraste,
|
||||
- keine Mehrfarbigkeit aus dem Akzent-Setup,
|
||||
- keine dauerhaften Animationen oder Parallax-Spielereien.
|
||||
|
||||
## 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“.
|
||||
Markenwirkung basiert auf neutralen, leicht warmen Flächen mit kontrolliertem Grün. Das Grün stammt aus dem Logo (Stapel-/Ordner-Idee) und steht für Ablage, Struktur und „self-hosted tool" statt „Social App". Akzent-Lime nur als gezielter Akzent-Glow im Branding und in Marketing-Sektionen.
|
||||
|
||||
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.
|
||||
Die App ist **light-first**, Dark-Mode ist gleichwertig zu pflegen. Neue Komponenten beziehen Farben aus Design-Tokens.
|
||||
|
||||
## Farbpalette
|
||||
|
||||
### Primärfarben
|
||||
- **Primary 700:** `#1C652F`
|
||||
Für Primärbuttons, aktive Icons, Fokusrahmen, Links in aktiven Zuständen.
|
||||
- **Primary 600:** `#2E7D32`
|
||||
Für Hover-Zustände und aktive Navigation.
|
||||
- **Primary 500:** `#3C8F42`
|
||||
Für ausgewählte Einträge, Badges, bestätigende States.
|
||||
- **Primary 300:** `#A8D5A2`
|
||||
Für weiche Hintergründe von Auswahlflächen.
|
||||
- **Primary 100:** `#EAF5E8`
|
||||
Für sehr subtile Hervorhebungen.
|
||||
### Primärfarben (Light)
|
||||
- **Primary 800:** `#10421E` – tiefster Markenton, Texte auf Light-Surface.
|
||||
- **Primary 700:** `#1C652F` – Primärbuttons, aktive Icons, Fokusrahmen.
|
||||
- **Primary 600:** `#2E7D32` – Hover-Zustände, aktive Navigation.
|
||||
- **Primary 500:** `#3C8F42` – ausgewählte Einträge, Badges, bestätigende States.
|
||||
- **Primary 300:** `#A8D5A2` – weiche Hintergründe für Auswahlflächen.
|
||||
- **Primary 100:** `#EAF5E8` – sehr subtile Hervorhebungen, Tints.
|
||||
- **Primary 050:** `#F4FAF1` – fast unsichtbare Pflasterfläche für Hero-Glows.
|
||||
|
||||
### Akzentfarbe
|
||||
- **Accent Lime:** `#B7E36B`
|
||||
Nur sehr sparsam einsetzen, z. B. kleiner Glow im Logo-Bereich, leichtere Highlights, Upload-Fortschritt oder ausgewählte Illustrationsdetails. Nicht für normalen Text.
|
||||
### Akzent
|
||||
- **Accent Lime:** `#B7E36B` – sparsam: Logo-Glow, kleine Highlights, Upload-Fortschritt.
|
||||
|
||||
### Neutrale Farben
|
||||
- **Background:** `#F6F8F5`
|
||||
Hauptseitenhintergrund.
|
||||
- **Surface 1:** `#FFFFFF`
|
||||
Karten, Panels, Modals, Dialoge.
|
||||
- **Surface 2:** `#F1F4EF`
|
||||
Sekundäre Flächen, Toolbar-Hintergründe, Tabellenkopf.
|
||||
- **Border:** `#DCE4D8`
|
||||
Standard-Border.
|
||||
- **Border Strong:** `#C7D2C2`
|
||||
Stärkere Abgrenzung bei Panels und Inputs.
|
||||
### Neutrale (Light)
|
||||
- **Background:** `#F5F8F2`
|
||||
- **Background Tint:** `#EEF3EA` – ambient Hintergrund-Verlauf.
|
||||
- **Surface:** `#FFFFFF` – Karten, Panels, Dialoge.
|
||||
- **Surface Alt:** `#F1F4EE` – Toolbar, Tabellenkopf, Sekundärflächen.
|
||||
- **Surface Elevated:** `#FBFCF8` – höhere Layer (Drawer-Inhalt, Dropdowns).
|
||||
- **Border:** `#DCE3D6`
|
||||
- **Border Strong:** `#C5CFBE`
|
||||
- **Border Subtle:** `#E8EDE2` – innere Trennlinien.
|
||||
|
||||
### Textfarben
|
||||
- **Text Primary:** `#1F2A21`
|
||||
- **Text Secondary:** `#5F6E62`
|
||||
- **Text Muted:** `#7D8A80`
|
||||
### Text (Light)
|
||||
- **Text Primary:** `#1A2A1E`
|
||||
- **Text Secondary:** `#5A6A5E`
|
||||
- **Text Muted:** `#7B897F`
|
||||
- **Text On Primary:** `#FFFFFF`
|
||||
|
||||
### Statusfarben
|
||||
Schlicht halten, nicht zu bunt.
|
||||
### Dark Mode
|
||||
Dark-Mode wird vollständig über Tokens gespiegelt. Eckwerte:
|
||||
- Background: `#0E1115`
|
||||
- Surface: `#161A20`
|
||||
- Surface Alt: `#1B2028`
|
||||
- Surface Elevated: `#1F252E`
|
||||
- Border: `#2A323D`
|
||||
- Border Strong: `#3A4452`
|
||||
- Text Primary: `#E9EFF3`
|
||||
- Text Secondary: `#B6BEC8`
|
||||
- Primary 700 (dark): `#5FB968`
|
||||
- Primary 600 (dark): `#4EA758`
|
||||
- Primary 100 (dark): `#1F2922`
|
||||
|
||||
### Status
|
||||
- **Success:** `#2E7D32`
|
||||
- **Warning:** `#B7791F`
|
||||
- **Danger:** `#C0392B`
|
||||
- **Info:** `#2F6FB3`
|
||||
|
||||
## Typografie
|
||||
Die Typografie soll neutral, gut lesbar und unauffällig modern sein. Keine dekorativen Schriften.
|
||||
Neutral, gut lesbar, unauffällig modern – keine dekorativen Schriften.
|
||||
|
||||
**Empfohlene Schriftfamilie:**
|
||||
- `Inter`
|
||||
- Fallback: `system-ui, sans-serif`
|
||||
**Stack:** `Inter, "Segoe UI", Roboto, system-ui, sans-serif`
|
||||
|
||||
**Typografische Regeln:**
|
||||
- normale Lesetexte: 14–15 px
|
||||
- UI-Haupttext in Listen und Tabellen: 14 px
|
||||
- Seitenüberschriften: 24–28 px
|
||||
- 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
|
||||
**Skala (rem-orientiert, fixe px-Breakpoints, keine viewport-Skalierung):**
|
||||
- `--font-size-2xs`: 11px – Status-Labels, Mini-Meta.
|
||||
- `--font-size-xs`: 12px – Tabellenmeta, Helper-Text.
|
||||
- `--font-size-sm`: 13px – sekundärer UI-Text.
|
||||
- `--font-size-md`: 14px – Standard-UI-Text, Listen.
|
||||
- `--font-size-lg`: 16px – akzentuierte UI-Hervorhebung.
|
||||
- `--font-size-xl`: 20px – Bereichsüberschriften.
|
||||
- `--font-size-2xl`: 26px – Seitenüberschriften.
|
||||
- `--font-size-3xl`: 32px – Display-Headlines (Hero).
|
||||
- `--font-size-display`: 44px – sehr seltene Marketing-Display-Texte.
|
||||
|
||||
**Schriftgewicht:**
|
||||
- 400 für normalen Fließtext
|
||||
- 500 für UI-Text und Labels
|
||||
- 600 für Titel und aktive Elemente
|
||||
- 700 nur sehr gezielt
|
||||
**Gewichte:**
|
||||
- 400 für Fließtext.
|
||||
- 500 für UI-Text und Labels.
|
||||
- 600 für Titel, Kicker, aktive Items.
|
||||
- 700 nur sehr gezielt (Display-Headlines, Logo-Wortmarke).
|
||||
|
||||
## Layoutprinzip
|
||||
Das Layout soll stark an eine Dateiverwaltung erinnern.
|
||||
**Letter-Spacing-Disziplin:**
|
||||
- Display-Headlines: leicht negatives Tracking (`-0.01em` bis `-0.02em`).
|
||||
- Kicker/Labels (uppercase): `0.05em` bis `0.08em`.
|
||||
- Standard: `0`.
|
||||
|
||||
### Grundaufbau
|
||||
- **Topbar** für Logo, Breadcrumbs, Kontextaktionen, Benutzer-Menü
|
||||
- **linke Sidebar** für Navigation
|
||||
- **Hauptbereich** für Dateiliste oder Grid
|
||||
- **rechte Vorschau / Detailansicht** optional als Panel oder getrennte Ansicht
|
||||
## Spacing
|
||||
Modulare Skala in 4er-Schritten:
|
||||
|
||||
### Seitenbreite und Abstand
|
||||
- großzügige horizontale Abstände
|
||||
- Hauptinhalte nicht zu schmal machen
|
||||
- Panels mit genug Luft, aber ohne Dashboard-Overdesign
|
||||
- Standard-Abstandssystem in 4er- oder 8er-Schritten
|
||||
```
|
||||
--space-1: 4px
|
||||
--space-2: 8px
|
||||
--space-3: 12px
|
||||
--space-4: 16px
|
||||
--space-5: 20px
|
||||
--space-6: 24px
|
||||
--space-7: 28px
|
||||
--space-8: 32px
|
||||
--space-10: 40px
|
||||
--space-12: 48px
|
||||
--space-16: 64px
|
||||
```
|
||||
|
||||
**Spacing-Skala:**
|
||||
- 4 px
|
||||
- 8 px
|
||||
- 12 px
|
||||
- 16 px
|
||||
- 20 px
|
||||
- 24 px
|
||||
- 32 px
|
||||
- 40 px
|
||||
Hauptseiten: `--hoard-page-width` 1120–1200px, Padding orientiert sich an `--space-6`/`--space-8`.
|
||||
|
||||
## Formensprache
|
||||
Die Formensprache soll weich, aber nicht rundgelutscht sein.
|
||||
## Formensprache (Border-Radien)
|
||||
- `--radius-xs`: 6px – kleine Pills, Status-Badges.
|
||||
- `--radius-sm`: 10px – Buttons, kleine Controls.
|
||||
- `--radius-md`: 14px – Inputs, Dropdowns, Listzeilen.
|
||||
- `--radius-lg`: 18px – Cards, Panels.
|
||||
- `--radius-xl`: 22px – Hero-Shells, Modals.
|
||||
- `--radius-full`: 999px – nur für Avatar/Tag-Pills.
|
||||
|
||||
- kleine Controls: `8px` Radius
|
||||
- Panels, Inputs, Dropdowns: `10px`
|
||||
- Modals und größere Karten: `14px`
|
||||
- keine pillenförmigen Vollflächen als Grundstil
|
||||
Keine pillenförmigen Vollflächen als Grundstil.
|
||||
|
||||
## Schatten und Tiefe
|
||||
Sehr zurückhaltend einsetzen. Die App soll stabil und ruhig wirken, nicht schwebend.
|
||||
## Schatten & Tiefe
|
||||
Mehrstufiges, sehr ruhiges Schatten-System:
|
||||
|
||||
**Standard-Schatten:**
|
||||
- Panels: leichter, weicher Schatten
|
||||
- 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
|
||||
```
|
||||
--shadow-xs: 0 1px 1px rgba(20, 30, 22, 0.04);
|
||||
--shadow-sm: 0 2px 6px rgba(20, 30, 22, 0.06);
|
||||
--shadow-md: 0 8px 22px rgba(20, 30, 22, 0.08);
|
||||
--shadow-lg: 0 18px 44px rgba(20, 30, 22, 0.12);
|
||||
--shadow-glow: 0 12px 36px rgba(28, 101, 47, 0.18);
|
||||
```
|
||||
|
||||
## Komponentenstil
|
||||
Regeln:
|
||||
- Panels: `--shadow-sm`.
|
||||
- Hover-Lift max. 2 px, nur bei klar interaktiven Elementen.
|
||||
- Dropdowns/Modals: `--shadow-md` bis `--shadow-lg`.
|
||||
- `--shadow-glow` ausschließlich für Brand-Bereiche (Login-Hero, Welcome-Hero).
|
||||
- Keine farbigen Schatten in Arbeitsflächen.
|
||||
|
||||
## Motion
|
||||
- Standard-Easing: `cubic-bezier(0.2, 0, 0, 1)`.
|
||||
- `--transition-fast`: 160 ms (Hover, Buttons, Listzeilen).
|
||||
- `--transition-medium`: 220 ms (Route-Wechsel, Banner).
|
||||
- `--transition-slow`: 320 ms (Hero-Enter, Drawer).
|
||||
- Globale Page-Enter-Animation: weicher Y-Slide + Fade (≤ 8 px).
|
||||
- `prefers-reduced-motion`: alle Transitions auf 1 ms reduzieren, keine Translates.
|
||||
|
||||
## App-Shell
|
||||
|
||||
### Topbar
|
||||
Die Topbar ist ruhig und funktional.
|
||||
- Höhe ca. 60–64 px
|
||||
- heller Hintergrund oder leicht abgesetzte Surface-Farbe
|
||||
- Logo links
|
||||
- Breadcrumbs klar lesbar, nicht zu klein
|
||||
- Kontextaktionen rechts davon oder am rechten Rand
|
||||
- dünne Unterkante oder subtile Shadow-Abgrenzung
|
||||
- Höhe ca. 64 px, opaker `surface`-Hintergrund, dünne Bottom-Border.
|
||||
- Branding links: Icon + Wortmarke + dezenter Subtext („Self-hosted Workspace").
|
||||
- Page-Kontext (Name + Beschreibung) als sekundäre Info, nur ab `>1180px`.
|
||||
- Rechts: Theme-Toggle, Account-Menü oder Login-Button.
|
||||
- Subtile bottom shadow (`--shadow-xs`).
|
||||
|
||||
### Sidebar
|
||||
Die Sidebar ist funktional, nicht dominant.
|
||||
- feste Breite, ca. 240–280 px
|
||||
- leicht abgesetzte Hintergrundfläche
|
||||
- aktive Einträge mit heller grüner Fläche und dunklerem Text
|
||||
- Icons schlicht und einheitlich
|
||||
- Navigation in logische Gruppen, aber ohne zu viele Sektionen
|
||||
- Breite 268–284 px, Hintergrund `surface-alt`, leicht abgesetzt.
|
||||
- Active-Indicator: linker, animierter, vertikaler Strich (`--color-primary-600`) + grüne Tint-Fläche + dunklerer Text.
|
||||
- Hover: leichte Tint-Fläche, kein Scaling.
|
||||
- Sektionen mit Kicker-Labels (`Navigation`, `Admin`).
|
||||
- Mindesthöhe pro Item: 44 px (Desktop), 48 px (Mobile).
|
||||
- Mobile = Bottom-Sheet-Drawer mit abgerundeten Top-Ecken.
|
||||
|
||||
### Dateiliste
|
||||
Die Dateiliste ist das Herzstück der App.
|
||||
### Footer
|
||||
- Sehr ruhig, kompakt, `surface` mit dünner oberer Border.
|
||||
- Links nur auf rechtlich/strukturell wichtige Routen (Impressum etc.).
|
||||
- Mobile: in Grid mit Tap-Größen ≥ 44 px.
|
||||
|
||||
**Darstellung:**
|
||||
- standardmäßig Listenansicht
|
||||
- klare Spalten für Name, Typ, Größe, geändert am
|
||||
- Zeilenhöhe eher luftig statt kompakt gepresst
|
||||
- Hover nur leicht hervorheben
|
||||
- ausgewählte Zeile mit heller grüner Tönung
|
||||
- Doppelklick oder klarer Primärklick zum Öffnen
|
||||
- Dateisymbole farblich dezent
|
||||
## Komponenten
|
||||
|
||||
**Wichtig:**
|
||||
Die Liste soll strukturierter und ruhiger wirken als ein typisches Admin-Grid. Nicht wie eine Datenbanktabelle, sondern wie eine echte Dateiverwaltung.
|
||||
|
||||
### Karten / Panels
|
||||
Karten nur dort einsetzen, wo es fachlich Sinn ergibt:
|
||||
- Vorschau-Panel
|
||||
- Datei-Infos
|
||||
- Upload-Status
|
||||
- Modals
|
||||
|
||||
Nicht jede Seite künstlich in zehn Karten aufteilen.
|
||||
### Cards / Panels
|
||||
- Hintergrund: `surface` mit subtilem 180°-Verlauf zu `surface-alt`.
|
||||
- Border: `--color-border`.
|
||||
- Radius: `--radius-lg`.
|
||||
- Padding: `--space-6` (Standard), kleinere Inhalte `--space-4`.
|
||||
- Hover (nur klar interaktive Cards): Border in Primary-300-Mix, `--shadow-md`, 2 px Y-Lift.
|
||||
|
||||
### Buttons
|
||||
Buttons sollen schlicht und klar sein.
|
||||
|
||||
**Primärbutton:**
|
||||
- grüner Hintergrund
|
||||
- weiße Schrift
|
||||
- mittlere Höhe
|
||||
- klar erkennbare Hover- und Disabled-Zustände
|
||||
|
||||
**Sekundärbutton:**
|
||||
- helle Fläche mit Border
|
||||
- dunkler Text
|
||||
- kein zu aggressiver Kontrast
|
||||
|
||||
**Tertiärbutton / Icon-Button:**
|
||||
- für Zeilenaktionen, Toolbar, Preview-Aktionen
|
||||
- Hover mit leichter Surface-Abhebung
|
||||
|
||||
**Regel:**
|
||||
Es sollte pro Bereich meist genau eine klare Primäraktion geben.
|
||||
- **Primary:** grüner Hintergrund (`--color-primary-700`), weiße Schrift, `--shadow-xs`. Hover: `--color-primary-600` + leichter Glow. Active: ohne Lift.
|
||||
- **Outlined:** transparenter Hintergrund, `--color-border-strong`, dunkler Text. Hover: leichte Primary-Tint-Fläche, Border zu Primary-Mix.
|
||||
- **Text/Tertiary:** für Zeilenaktionen, Toolbar, Nav. Hover: Surface-Tint.
|
||||
- **Icon-Buttons:** mind. 40×40 (Desktop), 44×44 (Mobile).
|
||||
- Letter-Spacing 0, kein Uppercase.
|
||||
- Pro Bereich genau **eine Primäraktion**.
|
||||
|
||||
### Inputs
|
||||
- weiße Fläche
|
||||
- klarer Border
|
||||
- Fokuszustand mit grünem Ring oder grün betonter Border
|
||||
- keine dunklen, schweren Inputs
|
||||
- Labels oberhalb statt Placeholder-only
|
||||
- Outline-Variante, `surface` Hintergrund.
|
||||
- Border `--color-border-strong`, Focus-Ring 3 px Primary-300-Tint + Primary-600-Border.
|
||||
- Labels oberhalb (Vuetify-Outlined-Label), keine reinen Placeholder.
|
||||
- Mind. 44 px Höhe auf Mobile.
|
||||
|
||||
### Modals und Dialoge
|
||||
- kompakt und funktional
|
||||
- deutlicher Titel
|
||||
- klare Primär- und Sekundäraktion
|
||||
- nicht zu breit
|
||||
- Löschen-Aktionen visuell bewusst neutral mit Danger-Akzent nur am Button
|
||||
### Listen / Tabellen
|
||||
- Tabelle: `surface` Hintergrund, abgesetzter Tabellen-Header (`surface-alt`).
|
||||
- Hover: sehr subtile Primary-Tint-Tönung.
|
||||
- Selected Row: Primary-100 Hintergrund, `--color-primary-700` Text.
|
||||
- Border-Bottom in `--color-border-subtle`.
|
||||
- `hoard-list-row` (Datei-/Item-Zeilen): luftiges Padding, klare Spalten, `transform: translateX(2px)` beim Hover.
|
||||
|
||||
### Dropdowns und Kontextmenüs
|
||||
- schlicht, hell, sauber getrennte Einträge
|
||||
- Icons optional, aber konsistent
|
||||
- Hover klar sichtbar
|
||||
- kritische Aktionen unten gruppieren
|
||||
### Status-Pills (`hoard-status`)
|
||||
- Kompakt, `--radius-xs`, mit dezentem Text/Background-Tint pro Status (Success/Info/Warning/Danger/Neutral).
|
||||
- Optional kleines Punktindikator-Dot vor dem Text.
|
||||
|
||||
## Vorschau-Bereich
|
||||
Der Vorschau-Bereich ist ein zentraler Teil von Hoard und soll hochwertig, aber ruhig wirken.
|
||||
### Banner-Stack
|
||||
- Position: fixiert unten rechts (Desktop), unten zentriert (Mobile).
|
||||
- Stapelbar, einzeln dismissable.
|
||||
- Opake Hintergründe, `--shadow-md`, sauber lesbar auf jedem Inhalt.
|
||||
- Enter/Leave: 180 ms Fade + 10 px Y.
|
||||
|
||||
### PDF-Vorschau
|
||||
- heller neutraler Hintergrund
|
||||
- PDF sitzt auf einer weißen „Papier“-Fläche
|
||||
- genug Rand um die Seite herum
|
||||
- Controls minimal und funktional
|
||||
## Hero-/Marketing-Bereiche
|
||||
- Erlauben einen **ambient Verlauf** (`hoard-panel-gradient` + Variablen).
|
||||
- Optional dezenter „Spotlight"-Glow (radialer Verlauf, `--color-primary-300` mit niedriger Opazität, blur).
|
||||
- Inhaltsbreite max ~64ch.
|
||||
- Display-Headlines mit `--font-size-3xl` oder `--font-size-display` (nur Hero).
|
||||
- Hero-Tags (`hoard-chip`/`hoard-tag`) als kompakte, dezent gerahmte Pills.
|
||||
|
||||
### Bildvorschau
|
||||
- dunklerer neutraler Viewer-Hintergrund ist okay, wenn das Bild dadurch besser wirkt
|
||||
- umgebende UI trotzdem konsistent mit dem restlichen Produkt halten
|
||||
- keine übertriebene Galerie-Optik
|
||||
## Vorschau-Bereich (Files)
|
||||
- **PDF-Vorschau:** neutraler Hintergrund, weiße „Papier"-Fläche mit `--shadow-md`, genug Rand. Controls minimal.
|
||||
- **Bildvorschau:** dunklerer neutraler Viewer-Hintergrund nur, wenn das Bild dadurch besser wirkt; UI-Chrome konsistent.
|
||||
- **Markdown-Editor/Reader:** wirkt wie ein Arbeitsdokument, nicht wie Blogpost. Lesbreite ~70ch, klare Heading-Hierarchie, sehr ruhige Codeblöcke.
|
||||
|
||||
### Markdown-Ansicht / Editor
|
||||
Markdown-Dateien sollen wie Arbeitsdokumente wirken, nicht wie Blogposts.
|
||||
|
||||
**Vorgaben:**
|
||||
- gute Textbreite
|
||||
- klare Hierarchie bei Überschriften
|
||||
- dezente Codeblock-Gestaltung
|
||||
- sehr gute Lesbarkeit
|
||||
- Editor und Preview optisch zur restlichen App passend, auch wenn `md-editor-v3` eigene Defaults mitbringt
|
||||
|
||||
## Tabellen- und Listenverhalten
|
||||
Da der Kern deiner App Listen, Dateiansichten und Metadaten sind, muss das Verhalten konsistent sein.
|
||||
|
||||
- Hover ist immer subtil
|
||||
- Auswahl ist immer über denselben Grünton markiert
|
||||
- aktive Navigation und aktive Listelemente nutzen dieselbe semantische Farbe
|
||||
- Sortierung, falls später vorhanden, visuell zurückhaltend markieren
|
||||
- Bulk-Actions nur anzeigen, wenn wirklich etwas ausgewählt ist
|
||||
|
||||
## Icons
|
||||
Empfohlen ist ein schlanker, moderner Icon-Stil mit einheitlicher Konturstärke.
|
||||
|
||||
Geeignet wären z. B.:
|
||||
- Lucide
|
||||
- Heroicons
|
||||
|
||||
**Regeln:**
|
||||
- möglichst Outline-Icons
|
||||
- nur wenige gefüllte Icons
|
||||
- Dateityp-Icons dürfen leicht differenziert sein, aber nicht bunt explodieren
|
||||
- Ordner-Icon in gedecktem Grün oder neutralem Grau
|
||||
|
||||
## Status und Feedback
|
||||
Feedback soll klar sein, aber nicht laut.
|
||||
|
||||
### Toasts
|
||||
- kurze Texte
|
||||
- kein unnötiger Fließtext
|
||||
- success, error, info klar unterscheidbar
|
||||
- am besten oben rechts oder unten rechts, aber konsistent
|
||||
|
||||
### Ladezustände
|
||||
- Skeletons oder sehr schlichte Loader
|
||||
- lieber ruhige Platzhalter statt hektische Spinner überall
|
||||
|
||||
### Leere Zustände
|
||||
Leere Zustände sollen freundlich, aber nüchtern sein.
|
||||
- kleines Icon oder einfache Illustration
|
||||
- ein klarer Satz
|
||||
- eine eindeutige Folgeaktion
|
||||
|
||||
## Login-Seite
|
||||
Die Login-Seite darf minimal etwas mehr Branding zeigen als die Haupt-App.
|
||||
|
||||
**Empfehlung:**
|
||||
- zentrierte Login-Card
|
||||
- Logo sichtbar
|
||||
- neutraler Hintergrund mit sehr leichter grüner Stimmung
|
||||
- keine starke Hero-Sektion nötig
|
||||
- Fokus auf schnellem Einstieg
|
||||
## Empty-/Loading-States
|
||||
- Empty: kleines Outline-Icon in Primary-Tint, kurzer Titel, ein Satz Begründung, eine klare CTA.
|
||||
- Loading: Skeletons (graue Pulslinien) bevorzugt, ansonsten ein kleiner zentraler Spinner mit beschreibendem Text. Keine flackernden Spinner-Felder.
|
||||
|
||||
## Responsive Verhalten
|
||||
Desktop ist der Hauptfokus. Mobile muss funktionieren, aber nicht die Priorität des MVP sein.
|
||||
Desktop ist Hauptfokus, Mobile muss aber sauber funktionieren.
|
||||
|
||||
### Desktop
|
||||
- volle Sidebar
|
||||
- großzügige Dateiliste
|
||||
- Preview neben Liste möglich
|
||||
### Breakpoints
|
||||
- `@media (width <= 1180px)` – Topbar-Kontextleiste verkürzen.
|
||||
- `@media (width <= 960px)` – Sidebar wird Bottom-Drawer; Karten-/Grid-Bereiche werden einspaltig; Spacing wird reduziert.
|
||||
- `@media (width <= 600px)` – CTAs full-width; Schriften minimal kompakter; Footer als Grid; Status-Pills behalten Lesbarkeit.
|
||||
|
||||
### Tablet
|
||||
- Sidebar einklappbar
|
||||
- Preview eher als Overlay oder eigene Ansicht
|
||||
|
||||
### Mobile
|
||||
- eher einfache Stapelansicht
|
||||
- Fokus auf Navigation und Öffnen
|
||||
- Bearbeitung von Markdown darf reduziert sein, solange Lesen und einfache Bedienung sauber funktionieren
|
||||
|
||||
### Umsetzungsstandard Responsivität (verbindlich)
|
||||
Die folgenden Regeln bilden den aktuellen Responsive-Standard von Hoard und sollen bei allen kommenden UI-Aufgaben eingehalten werden.
|
||||
|
||||
1. **Desktop-first, Mobile-only Overrides**
|
||||
- Desktop-Styles bleiben Basis.
|
||||
- Mobile-Anpassungen ausschließlich in Media Queries, keine Änderungen an Desktop-Baseregeln.
|
||||
|
||||
2. **Breakpoints**
|
||||
- `@media (width <= 960px)` für Tablet/Mobile-Umbruch (Layout-Stacks, Spacing-Reduktion, Drawer-/Footer-Verhalten).
|
||||
- `@media (width <= 600px)` für Phone-Feinschliff (volle Breite für CTAs, kompaktere Typo/Abstände).
|
||||
|
||||
3. **Globale Responsive-Patterns zuerst nutzen**
|
||||
- Wiederverwendbare Anpassungen immer zuerst in den globalen Dateien pflegen:
|
||||
- `GUI/src/global.css`
|
||||
- `GUI/src/styles/global/page-layouts.css`
|
||||
- `GUI/src/styles/global/surface-patterns.css`
|
||||
- Seiten-spezifisches `scoped` CSS nur für wirklich lokale Sonderfälle.
|
||||
|
||||
4. **Touch-Zielgrößen und Bedienbarkeit**
|
||||
- Interaktive Elemente mobil mit klarer Daumen-Bedienbarkeit:
|
||||
- Buttons mindestens `44px` Höhe.
|
||||
- Icon-Buttons mindestens `44x44px`.
|
||||
- Navigations-Listeneinträge mindestens `48px` Höhe.
|
||||
- Aktionszeilen (`hoard-action-row`) auf kleinen Geräten vertikal stapeln, damit Primäraktionen gut erreichbar sind.
|
||||
|
||||
5. **Safe-Area-Unterstützung**
|
||||
- Bei mobilen Außenabständen `env(safe-area-inset-*)` berücksichtigen (iOS/Android).
|
||||
- Muster: `max(var(--space-x), env(safe-area-inset-...))` als Fallback-sicherer Abstand.
|
||||
- Besonders relevant für App-Bar-Ränder, Seiten-Padding und Bottom-Bereiche (Drawer/Footer).
|
||||
|
||||
6. **App-Shell-Muster**
|
||||
- Mobile Navigation bleibt als Bottom-Sheet-Drawer.
|
||||
- Desktop-Navigation bleibt unverändert (keine visuelle Regression auf großen Viewports).
|
||||
- Footer-Links auf kleinen Screens umbauen (Wrap/Grid), mit ausreichend großen Tap-Flächen.
|
||||
|
||||
7. **Seiten-Muster**
|
||||
- Content-Änderungen vermeiden; nur Layout/Bedienung anpassen.
|
||||
- Karten-/Grid-Bereiche bei `<= 960px` in Einspalten-Layout überführen.
|
||||
- Primäre CTAs bei `<= 600px` in voller Breite anzeigen.
|
||||
|
||||
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.
|
||||
### Pflicht
|
||||
- Desktop-Baseregeln nicht anfassen, nur Mobile-Overrides per Media Query.
|
||||
- Touch-Größen: Buttons ≥ 44 px, Icon-Buttons ≥ 44×44, Nav-Items ≥ 48 px.
|
||||
- Safe-Areas via `env(safe-area-inset-*)` (Topbar-Padding, Footer/Drawer Bottom).
|
||||
- Banner-Stack respektiert Safe-Area Bottom.
|
||||
- Pflicht-Viewports im QA: `360x800`, `390x844`, `768x1024`, `1024x768`, `>=1280` – jeweils Light + Dark.
|
||||
|
||||
## Interaktionsprinzipien
|
||||
- Primäraktionen immer klar sichtbar
|
||||
- destruktive Aktionen nie zu nah an Standardaktionen
|
||||
- Dateizeilen sollen sich klickbar anfühlen, ohne wie Buttons auszusehen
|
||||
- Hover, Active und Selected Zustände deutlich unterscheiden
|
||||
- Fokuszustände für Tastaturbedienung sauber sichtbar machen
|
||||
|
||||
## Stil für konkrete Bereiche
|
||||
|
||||
### Ordnernavigation
|
||||
- Breadcrumbs schlicht, klickbar, gut lesbar
|
||||
- aktueller Ordner klar markiert
|
||||
- Pfad nie visuell dominanter als der Inhalt
|
||||
|
||||
### Upload-Bereich
|
||||
- Uploads funktional anzeigen, nicht dramatisch
|
||||
- Fortschritt mit ruhiger grüner Progressbar
|
||||
- Fehlerfälle klar lesbar
|
||||
- Upload-Liste eher kompakt halten
|
||||
|
||||
### Datei-Details
|
||||
- Metadaten in sauberem Zwei-Spalten-Raster oder kompakter Liste
|
||||
- Labels und Werte klar unterscheidbar
|
||||
- Aktionen wie Download, Umbenennen, Löschen klar getrennt
|
||||
- Primäraktionen klar sichtbar.
|
||||
- Destruktives nie direkt neben Standardaktion.
|
||||
- Dateizeilen sind klickbar, dürfen aber nicht wie Buttons aussehen.
|
||||
- Hover, Active und Selected klar unterscheidbar (unterschiedliche Tints/Outlines).
|
||||
- Fokus für Tastaturbedienung immer sichtbar (`outline: 2px solid var(--color-primary-500); outline-offset: 2px`).
|
||||
|
||||
## Designregeln für die Umsetzung
|
||||
|
||||
### Immer tun
|
||||
- viel Weißraum lassen
|
||||
- Grün nur gezielt einsetzen
|
||||
- Borders und Surface-Unterschiede subtil halten
|
||||
- Listen und Dateiansichten priorisieren
|
||||
- Text gut lesbar und eher neutral halten
|
||||
### Immer
|
||||
- Tokens nutzen (Farben, Spacing, Radius, Shadow).
|
||||
- Patterns wiederverwenden (`hoard-panel`, `hoard-page`, `hoard-action-row`, `hoard-kicker`, `hoard-status`, `hoard-chip`, `hoard-spotlight`).
|
||||
- Light- und Dark-Mode gleichwertig prüfen.
|
||||
- Animationen kurz halten und `prefers-reduced-motion` respektieren.
|
||||
|
||||
### Vermeiden
|
||||
- zu viele Karten
|
||||
- zu viele Farbflächen
|
||||
- übertriebene Animationen
|
||||
- mehrere konkurrierende Akzentfarben
|
||||
- rein dekorative UI-Elemente ohne Nutzen
|
||||
- zu viele Karten ohne Funktion,
|
||||
- zu viele Akzentfarben gleichzeitig,
|
||||
- harte 1:1-Kontraste,
|
||||
- echte Glasflächen / Backdrop-Blur,
|
||||
- dauernde Bewegungen,
|
||||
- Schriftgrößen, die mit der Viewport-Breite skalieren.
|
||||
|
||||
## Beispiel für Design Tokens
|
||||
Diese Tokens können später direkt in CSS-Variablen oder ein Theme übernommen werden.
|
||||
## Beispiel: Design-Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-bg: #F6F8F5;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-surface-alt: #F1F4EF;
|
||||
--color-border: #DCE4D8;
|
||||
--color-border-strong: #C7D2C2;
|
||||
/* Surfaces */
|
||||
--color-bg: #f5f8f2;
|
||||
--color-bg-tint: #eef3ea;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f1f4ee;
|
||||
--color-surface-elevated: #fbfcf8;
|
||||
|
||||
--color-text: #1F2A21;
|
||||
--color-text-secondary: #5F6E62;
|
||||
--color-text-muted: #7D8A80;
|
||||
--color-border: #dce3d6;
|
||||
--color-border-strong: #c5cfbe;
|
||||
--color-border-subtle: #e8ede2;
|
||||
|
||||
--color-primary-700: #1C652F;
|
||||
--color-primary-600: #2E7D32;
|
||||
--color-primary-500: #3C8F42;
|
||||
--color-primary-300: #A8D5A2;
|
||||
--color-primary-100: #EAF5E8;
|
||||
--color-accent-lime: #B7E36B;
|
||||
/* Text */
|
||||
--color-text: #1a2a1e;
|
||||
--color-text-secondary: #5a6a5e;
|
||||
--color-text-muted: #7b897f;
|
||||
--color-text-on-primary: #ffffff;
|
||||
|
||||
--color-success: #2E7D32;
|
||||
--color-warning: #B7791F;
|
||||
--color-danger: #C0392B;
|
||||
--color-info: #2F6FB3;
|
||||
/* Brand */
|
||||
--color-primary-800: #10421e;
|
||||
--color-primary-700: #1c652f;
|
||||
--color-primary-600: #2e7d32;
|
||||
--color-primary-500: #3c8f42;
|
||||
--color-primary-300: #a8d5a2;
|
||||
--color-primary-100: #eaf5e8;
|
||||
--color-primary-050: #f4faf1;
|
||||
--color-accent-lime: #b7e36b;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
/* Status */
|
||||
--color-success: #2e7d32;
|
||||
--color-warning: #b7791f;
|
||||
--color-danger: #c0392b;
|
||||
--color-info: #2f6fb3;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(16, 24, 18, 0.06);
|
||||
--shadow-md: 0 6px 18px rgba(16, 24, 18, 0.08);
|
||||
/* Radius */
|
||||
--radius-xs: 6px;
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 18px;
|
||||
--radius-xl: 22px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-xs: 0 1px 1px rgba(20, 30, 22, 0.04);
|
||||
--shadow-sm: 0 2px 6px rgba(20, 30, 22, 0.06);
|
||||
--shadow-md: 0 8px 22px rgba(20, 30, 22, 0.08);
|
||||
--shadow-lg: 0 18px 44px rgba(20, 30, 22, 0.12);
|
||||
--shadow-glow: 0 12px 36px rgba(28, 101, 47, 0.18);
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-7: 28px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* Motion */
|
||||
--transition-fast: 160ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--transition-medium: 220ms cubic-bezier(0.2, 0, 0, 1);
|
||||
--transition-slow: 320ms cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
```
|
||||
|
||||
## Abschlussentscheidung für Hoard
|
||||
Für Hoard ist ein **ruhiger, light-first, dateiorientierter Produktivstil mit neutralen Flächen und kontrolliertem Grün als Markenfarbe** die passendste Richtung.
|
||||
|
||||
Das passt zur Produktidee, weil:
|
||||
- die App primär eine Dateiverwaltung ist
|
||||
- Markdown-Bearbeitung und Vorschau im Vordergrund stehen
|
||||
- die Oberfläche einfach und wartbar bleiben soll
|
||||
- der Stil gut allein umsetzbar ist
|
||||
- die UI professionell wirkt, ohne nach großem SaaS-Produkt aussehen zu müssen
|
||||
|
||||
## Kurzfassung als Design-Leitlinie
|
||||
Wenn du bei einer UI-Entscheidung unsicher bist, gilt:
|
||||
Wenn du bei einer UI-Entscheidung unsicher bist:
|
||||
|
||||
**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.
|
||||
**Lieber präzise als spektakulär. Lieber ruhige Flächen mit Charakter als visuelle Effekte. Grün ist Identität, nicht Dekoration. Jedes Element muss eine Funktion haben – sonst fliegt es raus.**
|
||||
|
||||
Reference in New Issue
Block a user