Rename hoard- to ui- and add UI components

Mass rename of CSS classes, tokens and animations from the hoard- namespace to ui- (classes, variables like --ui-*, and keyframes). Introduces new UI components: EmptyState, StatusPill, and UserAvatar and updates admin views to import and use them. Updates many route/layout components and global.css to use the new ui- patterns and responsive variables. Also updates Impressum contact emails and adds .claude/settings.local.json to allow running npm scripts in the Claude local settings.
This commit is contained in:
Jonas
2026-04-28 21:52:22 +02:00
parent a512aaa0a7
commit 7e2ca4c9e2
19 changed files with 381 additions and 382 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npm run *)"
]
}
}
+8 -8
View File
@@ -45,18 +45,18 @@ Hoard ist ein bewusst einfach gehaltenes Solo-Projekt neben einer Ausbildung. Es
- Der modernisierte Look (siehe `GUI/style.md` → „Modernisierungs-Direktive") darf hochwertiger und visuell präziser wirken, bleibt aber ruhig, klar und produktiv kein Gaming-, Glassmorphism- oder Marketing-Look.
## Globale CSS-Basis
- `GUI/src/global.css` hält alle Design-Tokens (`:root`/`[data-theme='dark']`), Basis-Resets, Vuetify-Anpassungen und wiederverwendbare Patterns (`hoard-panel`, `hoard-toolbar`, `hoard-list-row`, `hoard-empty-state`, `hoard-status`, …).
- `GUI/src/styles/global/page-layouts.css` enthält Page-Shells (`hoard-page`, `hoard-page--centered`, `hoard-shell-grid`).
- `GUI/src/styles/global/surface-patterns.css` enthält wiederkehrende Surface-/Inhaltsbausteine (`hoard-kicker`, `hoard-action-row`, `hoard-panel-gradient`, `hoard-chip`, `hoard-spotlight` …).
- `GUI/src/global.css` hält alle Design-Tokens (`:root`/`[data-theme='dark']`), Basis-Resets, Vuetify-Anpassungen und wiederverwendbare Patterns (`ui-panel`, `ui-toolbar`, `ui-list-row`, `ui-empty-state`, `ui-status`, …).
- `GUI/src/styles/global/page-layouts.css` enthält Page-Shells (`ui-page`, `ui-page--centered`, `ui-shell-grid`).
- `GUI/src/styles/global/surface-patterns.css` enthält wiederkehrende Surface-/Inhaltsbausteine (`ui-kicker`, `ui-action-row`, `ui-panel-gradient`, `ui-chip`, `ui-spotlight` …).
- Alle drei werden in `GUI/src/main.ts` einmalig importiert.
- Neue Layouts/Patterns immer **zuerst** dort ergänzen, nicht in `scoped`-Styles duplizieren.
## Anleitung: CSS-Patterns verwenden
- Neue Seiten standardmäßig mit `hoard-page` aufbauen; für zentrierte Vollhöhen-Ansichten zusätzlich `hoard-page--centered`.
- Karten-/Shell-Container als `hoard-shell-grid hoard-panel` verwenden; Breite/Abstände pro Seite über CSS-Variablen setzen (`--hoard-shell-width`, `--hoard-shell-gap`, `--hoard-shell-padding`).
- Wiederkehrende Headlines/Kicker mit `hoard-kicker` (+ ggf. `--wide`/`--xs`).
- Aktionszeilen mit `hoard-action-row` bauen statt eigene Flex-Definitionen pro Seite.
- Gradient-Flächen über `hoard-panel-gradient` + Variablen steuern.
- Neue Seiten standardmäßig mit `ui-page` aufbauen; für zentrierte Vollhöhen-Ansichten zusätzlich `ui-page--centered`.
- Karten-/Shell-Container als `ui-shell-grid ui-panel` verwenden; Breite/Abstände pro Seite über CSS-Variablen setzen (`--ui-shell-width`, `--ui-shell-gap`, `--ui-shell-padding`).
- Wiederkehrende Headlines/Kicker mit `ui-kicker` (+ ggf. `--wide`/`--xs`).
- Aktionszeilen mit `ui-action-row` bauen statt eigene Flex-Definitionen pro Seite.
- Gradient-Flächen über `ui-panel-gradient` + Variablen steuern.
- Lokales `scoped` CSS nur für wirklich seitenspezifische Sonderfälle.
## Aktueller Stand (Identity-Branch)
+24 -44
View File
@@ -176,8 +176,8 @@ watch(
</script>
<template>
<v-app class="hoard-shell">
<v-app-bar class="hoard-app-bar" elevation="0" height="68">
<v-app class="ui-shell">
<v-app-bar class="ui-app-bar" elevation="0" height="68">
<template #prepend>
<v-app-bar-nav-icon
v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'"
@@ -314,7 +314,7 @@ watch(
<v-navigation-drawer
v-model="showDrawer"
:class="['hoard-drawer', { 'hoard-drawer--mobile': display.mobile.value }]"
:class="['ui-drawer', { 'ui-drawer--mobile': display.mobile.value }]"
:location="display.mobile.value ? 'bottom' : 'left'"
:temporary="display.mobile.value"
:permanent="!display.mobile.value"
@@ -322,7 +322,7 @@ watch(
:elevation="display.mobile.value ? 6 : 0"
>
<div class="drawer-top">
<p class="drawer-kicker hoard-kicker hoard-kicker--xs">Navigation</p>
<p class="drawer-kicker ui-kicker ui-kicker--xs">Navigation</p>
</div>
<v-list nav :density="display.mobile.value ? 'default' : 'comfortable'" class="px-1">
@@ -333,15 +333,15 @@ watch(
:active="route.path === item.path"
:prepend-icon="item.icon"
:title="item.name"
class="hoard-nav-item"
class="ui-nav-item"
rounded="md"
link
/>
<template v-if="adminSidebarRoutes.length > 0">
<hr class="hoard-divider-soft drawer-section-divider" />
<hr class="ui-divider-soft drawer-section-divider" />
<div class="drawer-section-head">
<p class="drawer-kicker hoard-kicker hoard-kicker--xs">Admin</p>
<p class="drawer-kicker ui-kicker ui-kicker--xs">Admin</p>
</div>
<v-list-item
@@ -351,7 +351,7 @@ watch(
:active="route.path === item.path"
:prepend-icon="item.icon"
:title="item.name"
class="hoard-nav-item"
class="ui-nav-item"
rounded="md"
link
/>
@@ -359,28 +359,8 @@ watch(
</v-list>
<template #append>
<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>
<div v-if="!isAuthenticated" class="drawer-bottom">
<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"
@@ -392,7 +372,7 @@ watch(
</template>
</v-navigation-drawer>
<v-main class="hoard-main">
<v-main class="ui-main">
<div class="main-shell">
<router-view v-slot="{ Component }">
<transition name="route-fade" mode="out-in">
@@ -403,7 +383,7 @@ watch(
<v-footer
v-if="shouldShowFooter"
class="hoard-footer"
class="ui-footer"
>
<div class="footer-inner">
<div class="footer-brand">
@@ -455,11 +435,11 @@ watch(
</template>
<style scoped>
.hoard-shell {
.ui-shell {
min-height: 100vh;
}
.hoard-app-bar {
.ui-app-bar {
padding-inline: var(--space-3);
}
@@ -676,11 +656,11 @@ watch(
}
/* ---------- Drawer ---------- */
.hoard-drawer {
.ui-drawer {
padding-top: var(--space-2);
}
.hoard-drawer--mobile {
.ui-drawer--mobile {
border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl);
max-height: min(74vh, 580px);
@@ -702,11 +682,11 @@ watch(
padding: 0 var(--space-5) var(--space-2);
}
:deep(.hoard-nav-item) {
:deep(.ui-nav-item) {
min-height: 44px;
}
:deep(.hoard-nav-item .v-list-item-title) {
:deep(.ui-nav-item .v-list-item-title) {
font-weight: 500;
}
@@ -768,7 +748,7 @@ watch(
}
/* ---------- Main ---------- */
.hoard-main {
.ui-main {
display: flex;
flex-direction: column;
min-height: calc(100vh - 68px);
@@ -797,7 +777,7 @@ watch(
}
/* ---------- Footer ---------- */
.hoard-footer {
.ui-footer {
flex: 0 0 auto;
height: auto !important;
min-height: 0 !important;
@@ -927,7 +907,7 @@ watch(
/* ---------- Responsive ---------- */
@media (width <= 960px) {
.hoard-app-bar {
.ui-app-bar {
padding-inline:
max(var(--space-1), env(safe-area-inset-left))
max(var(--space-1), env(safe-area-inset-right));
@@ -962,7 +942,7 @@ watch(
display: none;
}
.hoard-drawer {
.ui-drawer {
padding-top: var(--space-1);
}
@@ -995,9 +975,9 @@ watch(
}
.app-banner-stack {
right: var(--hoard-mobile-safe-right);
left: var(--hoard-mobile-safe-left);
bottom: calc(var(--hoard-mobile-safe-bottom) + var(--space-1));
right: var(--ui-mobile-safe-right);
left: var(--ui-mobile-safe-left);
bottom: calc(var(--ui-mobile-safe-bottom) + var(--space-1));
}
.app-banner-list {
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{
icon: string
title: string
}>()
</script>
<template>
<section class="ui-panel ui-empty-state">
<span class="ui-icon-tile ui-icon-tile--lg">
<v-icon :icon="icon" size="22" />
</span>
<h2>{{ title }}</h2>
<p><slot /></p>
</section>
</template>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
defineProps<{
variant: 'success' | 'danger' | 'warning' | 'info' | 'muted'
}>()
</script>
<template>
<span :class="['ui-status', `ui-status--${variant}`]">
<slot />
</span>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
defineProps<{
initials: string
}>()
</script>
<template>
<span>{{ initials }}</span>
</template>
+33 -33
View File
@@ -506,7 +506,7 @@ pre {
}
/* ---------- Status pills ---------- */
.hoard-status {
.ui-status {
display: inline-flex;
align-items: center;
gap: var(--space-2);
@@ -518,7 +518,7 @@ pre {
white-space: nowrap;
}
.hoard-status::before {
.ui-status::before {
content: '';
width: 6px;
height: 6px;
@@ -527,37 +527,37 @@ pre {
box-shadow: 0 0 0 3px color-mix(in srgb, currentcolor 22%, transparent);
}
.hoard-status--success {
.ui-status--success {
color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 14%, var(--color-surface) 86%);
}
.hoard-status--warning {
.ui-status--warning {
color: var(--color-warning);
background-color: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface) 84%);
}
.hoard-status--danger {
.ui-status--danger {
color: var(--color-danger);
background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface) 86%);
}
.hoard-status--info {
.ui-status--info {
color: var(--color-info);
background-color: color-mix(in srgb, var(--color-info) 14%, var(--color-surface) 86%);
}
.hoard-status--muted {
.ui-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 {
.ui-status--muted::before {
background-color: var(--color-text-muted);
}
/* ---------- Reusable surfaces ---------- */
.hoard-panel {
.ui-panel {
position: relative;
background:
linear-gradient(
@@ -574,19 +574,19 @@ pre {
transform var(--transition-fast);
}
.hoard-panel--elevated {
.ui-panel--elevated {
background-color: var(--color-surface-elevated);
box-shadow: var(--shadow-md);
}
.hoard-panel--ghost {
.ui-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 {
.ui-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
@@ -598,7 +598,7 @@ pre {
border-top-right-radius: inherit;
}
.hoard-list-row {
.ui-list-row {
display: grid;
grid-template-columns: minmax(220px, 2fr) minmax(120px, 1fr) minmax(100px, 1fr) minmax(120px, 1fr);
align-items: center;
@@ -610,7 +610,7 @@ pre {
transform var(--transition-fast);
}
.hoard-list-row:hover {
.ui-list-row:hover {
background-color: color-mix(
in srgb,
var(--color-primary-100) 32%,
@@ -619,7 +619,7 @@ pre {
transform: translateX(2px);
}
.hoard-list-row.is-selected {
.ui-list-row.is-selected {
color: var(--color-primary-700);
background-color: color-mix(
in srgb,
@@ -628,24 +628,24 @@ pre {
);
}
.hoard-meta {
.ui-meta {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
.hoard-empty-state {
.ui-empty-state {
padding: var(--space-10) var(--space-6);
text-align: center;
color: var(--color-text-secondary);
}
.hoard-empty-state h2 {
.ui-empty-state h2 {
margin-bottom: var(--space-2);
font-size: var(--font-size-xl);
font-weight: 600;
}
.hoard-empty-state p {
.ui-empty-state p {
margin: 0 auto;
max-width: 44ch;
}
@@ -678,7 +678,7 @@ pre {
}
/* ---------- Animations ---------- */
@keyframes hoard-soft-enter {
@keyframes ui-soft-enter {
from {
opacity: 0;
transform: translateY(8px);
@@ -690,7 +690,7 @@ pre {
}
}
@keyframes hoard-pulse-ring {
@keyframes ui-pulse-ring {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary-300) 35%, transparent);
}
@@ -716,7 +716,7 @@ pre {
.v-btn,
.v-navigation-drawer .v-list-item,
.hoard-list-row {
.ui-list-row {
transform: none !important;
}
}
@@ -724,13 +724,13 @@ pre {
/* ---------- Responsive ---------- */
@media (width <= 960px) {
:root {
--hoard-mobile-safe-left: max(var(--space-2), env(safe-area-inset-left));
--hoard-mobile-safe-right: max(var(--space-2), env(safe-area-inset-right));
--hoard-mobile-safe-bottom: max(var(--space-3), env(safe-area-inset-bottom));
--ui-mobile-safe-left: max(var(--space-2), env(safe-area-inset-left));
--ui-mobile-safe-right: max(var(--space-2), env(safe-area-inset-right));
--ui-mobile-safe-bottom: max(var(--space-3), env(safe-area-inset-bottom));
}
.v-main {
padding-bottom: var(--hoard-mobile-safe-bottom);
padding-bottom: var(--ui-mobile-safe-bottom);
}
.v-btn {
@@ -747,40 +747,40 @@ pre {
margin: 4px var(--space-2);
}
.hoard-list-row {
.ui-list-row {
grid-template-columns: 1fr;
gap: var(--space-2);
align-items: start;
padding-block: var(--space-4);
}
.hoard-toolbar {
.ui-toolbar {
flex-wrap: wrap;
align-items: stretch;
padding: var(--space-3);
}
.hoard-empty-state {
.ui-empty-state {
padding: var(--space-8) var(--space-4);
}
.v-navigation-drawer {
border-right: none !important;
border-top: 1px solid var(--color-border) !important;
padding-bottom: var(--hoard-mobile-safe-bottom);
padding-bottom: var(--ui-mobile-safe-bottom);
}
}
@media (width <= 600px) {
.hoard-toolbar {
.ui-toolbar {
gap: var(--space-2);
}
.hoard-list-row {
.ui-list-row {
padding-inline: var(--space-3);
}
.hoard-meta {
.ui-meta {
font-size: var(--font-size-xs);
}
}
+10 -10
View File
@@ -72,8 +72,8 @@ onBeforeUnmount(() => {
</script>
<template>
<v-container fluid class="not-found-page hoard-page hoard-page--centered">
<section class="not-found-shell hoard-panel hoard-panel-gradient hoard-spotlight hoard-shell-grid">
<v-container fluid class="not-found-page ui-page ui-page--centered">
<section class="not-found-shell ui-panel ui-panel-gradient ui-spotlight ui-shell-grid">
<div class="not-found-visual">
<div class="image-frame">
<img
@@ -86,14 +86,14 @@ onBeforeUnmount(() => {
<div class="not-found-content">
<p class="not-found-code">404</p>
<p class="hoard-kicker hoard-kicker--wide">Seite nicht gefunden</p>
<p class="ui-kicker ui-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. Wir leiten dich gleich zur
{{ autoRedirectTargetLabel }} weiter oder du nutzt direkt einen der Buttons unten.
</p>
<div class="not-found-actions hoard-action-row">
<div class="not-found-actions ui-action-row">
<v-btn
variant="elevated"
:prepend-icon="redirectButtonIcon"
@@ -117,11 +117,11 @@ onBeforeUnmount(() => {
<style scoped>
.not-found-shell {
--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%;
--ui-shell-width: 1040px;
--ui-gradient-angle: 130deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr);
border-radius: var(--radius-xl);
@@ -211,7 +211,7 @@ h1 {
@media (prefers-reduced-motion: no-preference) {
.not-found-visual,
.not-found-content {
animation: hoard-soft-enter 320ms both;
animation: ui-soft-enter 320ms both;
}
.not-found-content {
+10 -10
View File
@@ -47,15 +47,15 @@ onMounted(() => {
</script>
<template>
<v-container fluid class="forbidden-page hoard-page hoard-page--centered">
<section class="forbidden-shell hoard-panel hoard-panel-gradient hoard-spotlight">
<v-container fluid class="forbidden-page ui-page ui-page--centered">
<section class="forbidden-shell ui-panel ui-panel-gradient ui-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 hoard-kicker--wide">Fehlende Berechtigung</p>
<p class="ui-kicker ui-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,
@@ -63,7 +63,7 @@ onMounted(() => {
</p>
</header>
<div class="forbidden-actions hoard-action-row">
<div class="forbidden-actions ui-action-row">
<v-btn
variant="elevated"
:prepend-icon="primaryActionIcon"
@@ -83,14 +83,14 @@ onMounted(() => {
<style scoped>
.forbidden-page {
--hoard-centered-offset: 200px;
--ui-centered-offset: 200px;
}
.forbidden-shell {
--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%;
--ui-gradient-angle: 130deg;
--ui-gradient-start: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface) 86%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
display: flex;
flex-direction: column;
@@ -152,7 +152,7 @@ onMounted(() => {
@media (prefers-reduced-motion: no-preference) {
.forbidden-shell {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
}
+35 -35
View File
@@ -71,10 +71,10 @@ const techStack = [
</script>
<template>
<v-container fluid class="landing-page hoard-page">
<section class="hero hoard-panel hoard-panel-gradient hoard-spotlight">
<v-container fluid class="landing-page ui-page">
<section class="hero ui-panel ui-panel-gradient ui-spotlight">
<div class="hero-copy">
<p class="hero-kicker hoard-kicker hoard-kicker--wide">Self-hosted Datei-Workspace</p>
<p class="hero-kicker ui-kicker ui-kicker--wide">Self-hosted Datei-Workspace</p>
<h1>
Eine ruhige Heimat für deine
<span class="hero-accent">Dateien, Ordner und Notizen.</span>
@@ -84,7 +84,7 @@ const techStack = [
Daten, Struktur und Workflow behalten wollen ohne Cloud-Lock-in, ohne SaaS-Abo.
</p>
<div class="hero-actions hoard-action-row">
<div class="hero-actions ui-action-row">
<v-btn variant="elevated" size="large" prepend-icon="mdi-login" to="/login">
Anmelden
</v-btn>
@@ -94,13 +94,13 @@ const techStack = [
</div>
<div class="hero-tags">
<span class="hoard-chip hoard-chip--brand">
<span class="ui-chip ui-chip--brand">
<v-icon icon="mdi-shield-check-outline" size="14" /> Datenhoheit
</span>
<span class="hoard-chip">
<span class="ui-chip">
<v-icon icon="mdi-account-multiple-outline" size="14" /> Mehrbenutzerfähig
</span>
<span class="hoard-chip">
<span class="ui-chip">
<v-icon icon="mdi-weather-night" size="14" /> Light- und Dark-Mode
</span>
</div>
@@ -120,7 +120,7 @@ const techStack = [
<div class="preview-window__body">
<header class="preview-toolbar">
<span class="preview-toolbar__title">Dokumentation</span>
<span class="hoard-chip hoard-chip--brand">
<span class="ui-chip ui-chip--brand">
<v-icon icon="mdi-folder-outline" size="14" /> 3 Ordner · 12 Dateien
</span>
</header>
@@ -134,7 +134,7 @@ const techStack = [
<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>
<span class="ui-status ui-status--muted">Geteilt</span>
</article>
<article class="preview-row preview-row--active">
@@ -145,7 +145,7 @@ const techStack = [
<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>
<span class="ui-status ui-status--success">Editor</span>
</article>
<article class="preview-row">
@@ -156,7 +156,7 @@ const techStack = [
<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>
<span class="ui-status ui-status--info">Vorschau</span>
</article>
<article class="preview-row">
@@ -167,7 +167,7 @@ const techStack = [
<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>
<span class="ui-status ui-status--muted">Bereit</span>
</article>
</div>
</div>
@@ -180,8 +180,8 @@ const techStack = [
</section>
<section class="value-grid">
<article v-for="item in valueProps" :key="item.title" class="value-card hoard-panel">
<span class="hoard-icon-tile hoard-icon-tile--lg">
<article v-for="item in valueProps" :key="item.title" class="value-card ui-panel">
<span class="ui-icon-tile ui-icon-tile--lg">
<v-icon :icon="item.icon" size="22" />
</span>
<h2>{{ item.title }}</h2>
@@ -189,16 +189,16 @@ const techStack = [
</article>
</section>
<section class="feature-section hoard-panel">
<header class="hoard-section-head">
<p class="hoard-kicker">Für den Produktivalltag</p>
<section class="feature-section ui-panel">
<header class="ui-section-head">
<p class="ui-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">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon :icon="feature.icon" size="20" />
</span>
<div>
@@ -210,12 +210,12 @@ const techStack = [
</section>
<section class="workflow-section">
<header class="hoard-section-head">
<p class="hoard-kicker">So funktioniert Hoard</p>
<header class="ui-section-head">
<p class="ui-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">
<article v-for="step in workflowSteps" :key="step.number" class="workflow-card ui-panel">
<p class="workflow-number">{{ step.number }}</p>
<h3>{{ step.title }}</h3>
<p>{{ step.text }}</p>
@@ -223,9 +223,9 @@ const techStack = [
</div>
</section>
<section class="stack-section hoard-panel hoard-panel-gradient">
<section class="stack-section ui-panel ui-panel-gradient">
<div class="stack-copy">
<p class="hoard-kicker">Technische Basis</p>
<p class="ui-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,
@@ -244,15 +244,15 @@ const techStack = [
<style scoped>
.landing-page {
--hoard-page-width: 1200px;
--ui-page-width: 1200px;
}
/* ---------- Hero ---------- */
.hero {
--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: 60%;
--ui-gradient-angle: 130deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 60%;
position: relative;
display: grid;
@@ -586,10 +586,10 @@ h1 {
/* ---------- 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%;
--ui-gradient-angle: 110deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 50%, var(--color-surface) 50%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 70%;
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
@@ -648,7 +648,7 @@ h1 {
.feature-section,
.workflow-section,
.stack-section {
animation: hoard-soft-enter 320ms both;
animation: ui-soft-enter 320ms both;
}
.hero-preview {
@@ -673,7 +673,7 @@ h1 {
}
.preview-row {
animation: hoard-soft-enter 240ms both;
animation: ui-soft-enter 240ms both;
}
.preview-row:nth-child(2) {
@@ -778,7 +778,7 @@ h1 {
grid-template-columns: auto 1fr;
}
.preview-row .hoard-status {
.preview-row .ui-status {
display: none;
}
}
+24 -24
View File
@@ -13,8 +13,8 @@ const registerDetails = [
const contactDetails = [
{ label: 'Telefon', value: '+49 30 1234567-0', href: 'tel:+493012345670' },
{ label: 'E-Mail', value: 'kontakt@hoard-demo.de', href: 'mailto:kontakt@hoard-demo.de' },
{ label: 'Support', value: 'support@hoard-demo.de', href: 'mailto:support@hoard-demo.de' },
{ label: 'E-Mail', value: 'kontakt@ui-demo.de', href: 'mailto:kontakt@ui-demo.de' },
{ label: 'Support', value: 'support@ui-demo.de', href: 'mailto:support@ui-demo.de' },
]
const legalNotes = [
@@ -42,10 +42,10 @@ const legalNotes = [
</script>
<template>
<v-container fluid class="impressum-page hoard-page">
<section class="impressum-hero hoard-panel hoard-panel-gradient hoard-spotlight">
<v-container fluid class="impressum-page ui-page">
<section class="impressum-hero ui-panel ui-panel-gradient ui-spotlight">
<div class="impressum-hero__copy">
<p class="hoard-kicker hoard-kicker--wide">Rechtliche Angaben</p>
<p class="ui-kicker ui-kicker--wide">Rechtliche Angaben</p>
<h1>Impressum</h1>
<p class="impressum-hero__lead">
Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Vor produktivem
@@ -53,16 +53,16 @@ const legalNotes = [
</p>
<div class="impressum-hero__meta">
<span class="hoard-chip hoard-chip--brand">
<span class="ui-chip ui-chip--brand">
<v-icon icon="mdi-information-outline" size="14" /> Testdaten
</span>
<span class="hoard-chip">
<span class="ui-chip">
<v-icon icon="mdi-calendar-month-outline" size="14" /> Stand: 26. April 2026
</span>
</div>
</div>
<div class="impressum-hero__actions hoard-action-row">
<div class="impressum-hero__actions ui-action-row">
<v-btn variant="elevated" prepend-icon="mdi-home-outline" to="/welcome">
Zur Startseite
</v-btn>
@@ -71,9 +71,9 @@ const legalNotes = [
</section>
<section class="details-grid">
<article class="detail-card hoard-panel">
<article class="detail-card ui-panel">
<header class="detail-card__head">
<span class="hoard-icon-tile"><v-icon icon="mdi-domain" size="20" /></span>
<span class="ui-icon-tile"><v-icon icon="mdi-domain" size="20" /></span>
<h2>Anbieterangaben</h2>
</header>
<dl class="detail-list">
@@ -84,9 +84,9 @@ const legalNotes = [
</dl>
</article>
<article class="detail-card hoard-panel">
<article class="detail-card ui-panel">
<header class="detail-card__head">
<span class="hoard-icon-tile"><v-icon icon="mdi-email-outline" size="20" /></span>
<span class="ui-icon-tile"><v-icon icon="mdi-email-outline" size="20" /></span>
<h2>Kontakt</h2>
</header>
<dl class="detail-list">
@@ -99,9 +99,9 @@ const legalNotes = [
</dl>
</article>
<article class="detail-card hoard-panel">
<article class="detail-card ui-panel">
<header class="detail-card__head">
<span class="hoard-icon-tile"><v-icon icon="mdi-clipboard-text-outline" size="20" /></span>
<span class="ui-icon-tile"><v-icon icon="mdi-clipboard-text-outline" size="20" /></span>
<h2>Register &amp; Steuer</h2>
</header>
<dl class="detail-list">
@@ -113,16 +113,16 @@ const legalNotes = [
</article>
</section>
<section class="notes-section hoard-panel">
<header class="hoard-section-head">
<p class="hoard-kicker">Rechtliche Hinweise</p>
<section class="notes-section ui-panel">
<header class="ui-section-head">
<p class="ui-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">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon :icon="note.icon" size="20" />
</span>
<div>
@@ -137,15 +137,15 @@ const legalNotes = [
<style scoped>
.impressum-page {
--hoard-page-width: 1180px;
--ui-page-width: 1180px;
}
/* ---------- Hero ---------- */
.impressum-hero {
--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%;
--ui-gradient-angle: 130deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
display: grid;
grid-template-columns: minmax(0, 1.2fr) auto;
@@ -296,7 +296,7 @@ dd {
.impressum-hero,
.detail-card,
.notes-section {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
.detail-card:nth-child(2) { animation-delay: 80ms; }
+20 -28
View File
@@ -4,6 +4,8 @@ import { useRoute, useRouter } from 'vue-router'
import { fetchAdminUserById, type AdminUser } from '@/services/adminUsers'
import { AuthRequestError } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners'
import StatusPill from '@/components/ui/StatusPill.vue'
import UserAvatar from '@/components/ui/UserAvatar.vue'
const route = useRoute()
const router = useRouter()
@@ -95,15 +97,15 @@ onMounted(() => {
</script>
<template>
<v-container fluid class="admin-user-detail-page hoard-page">
<header class="admin-user-detail-head hoard-panel hoard-panel-gradient">
<v-container fluid class="admin-user-detail-page ui-page">
<header class="admin-user-detail-head ui-panel ui-panel-gradient">
<div class="admin-user-detail-head__copy">
<p class="hoard-kicker hoard-kicker--wide">Adminbereich</p>
<p class="ui-kicker ui-kicker--wide">Adminbereich</p>
<h1>Benutzerdetails</h1>
<p>Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.</p>
</div>
<div class="admin-user-detail-head__actions hoard-action-row">
<div class="admin-user-detail-head__actions ui-action-row">
<v-btn
variant="outlined"
prepend-icon="mdi-arrow-left"
@@ -129,35 +131,25 @@ onMounted(() => {
<p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen </p>
<article v-else-if="user" class="admin-user-detail-card hoard-panel">
<article v-else-if="user" class="admin-user-detail-card ui-panel">
<header class="admin-user-detail-card__head">
<span class="admin-user-detail-avatar">{{ userInitials }}</span>
<UserAvatar class="admin-user-detail-avatar" :initials="userInitials" />
<div>
<p class="hoard-kicker hoard-kicker--xs">Konto</p>
<p class="ui-kicker ui-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',
]"
>
<StatusPill :variant="user.isActive ? 'success' : 'danger'">
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
</StatusPill>
<StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
{{ user.mustChangePassword ? 'Passwort wechseln' : 'Passwort aktuell' }}
</span>
</StatusPill>
</div>
</header>
<hr class="hoard-divider-soft" />
<hr class="ui-divider-soft" />
<dl class="admin-user-detail-grid">
<div class="admin-user-detail-item">
@@ -187,14 +179,14 @@ onMounted(() => {
<style scoped>
.admin-user-detail-page {
--hoard-page-width: 1080px;
--ui-page-width: 1080px;
}
.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%;
--ui-gradient-angle: 120deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
display: grid;
grid-template-columns: minmax(0, 1.1fr) auto;
@@ -315,7 +307,7 @@ onMounted(() => {
@media (prefers-reduced-motion: no-preference) {
.admin-user-detail-head,
.admin-user-detail-card {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
.admin-user-detail-card {
+37 -54
View File
@@ -4,6 +4,9 @@ import { useRouter } from 'vue-router'
import { fetchAdminUsers, type AdminUser } from '@/services/adminUsers'
import { AuthRequestError } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners'
import StatusPill from '@/components/ui/StatusPill.vue'
import UserAvatar from '@/components/ui/UserAvatar.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
const router = useRouter()
const appBannersStore = useAppBannersStore()
@@ -111,15 +114,15 @@ onMounted(() => {
</script>
<template>
<v-container fluid class="admin-users-page hoard-page">
<header class="admin-users-header hoard-panel hoard-panel-gradient">
<v-container fluid class="admin-users-page ui-page">
<header class="admin-users-header ui-panel ui-panel-gradient">
<div class="admin-users-header__copy">
<p class="hoard-kicker hoard-kicker--wide">Adminbereich</p>
<p class="ui-kicker ui-kicker--wide">Adminbereich</p>
<h1>Benutzerverwaltung</h1>
<p>Alle Hoard-Konten mit Rollen, Status und Passwortwechselpflicht read-only.</p>
</div>
<div class="admin-users-header__actions hoard-action-row">
<div class="admin-users-header__actions ui-action-row">
<v-text-field
v-model="searchQuery"
variant="outlined"
@@ -144,7 +147,7 @@ onMounted(() => {
<section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht">
<article class="admin-users-stat">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-account-group-outline" size="20" />
</span>
<div>
@@ -153,7 +156,7 @@ onMounted(() => {
</div>
</article>
<article class="admin-users-stat">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-account-check-outline" size="20" />
</span>
<div>
@@ -162,7 +165,7 @@ onMounted(() => {
</div>
</article>
<article class="admin-users-stat">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-shield-account-outline" size="20" />
</span>
<div>
@@ -171,7 +174,7 @@ onMounted(() => {
</div>
</article>
<article class="admin-users-stat">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-lock-reset" size="20" />
</span>
<div>
@@ -187,16 +190,16 @@ onMounted(() => {
<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>
<EmptyState
v-else-if="!hasUsers"
icon="mdi-account-question-outline"
title="Keine Benutzer gefunden"
>
Aktuell sind keine Konten vorhanden. Ein neuer Account muss vom Admin manuell angelegt werden.
</EmptyState>
<section v-else class="admin-users-listing hoard-panel">
<header class="admin-users-listing__head hoard-toolbar">
<section v-else class="admin-users-listing ui-panel">
<header class="admin-users-listing__head ui-toolbar">
<div>
<p class="admin-users-listing__title">Benutzer</p>
<p class="admin-users-listing__meta">{{ filteredUsers.length }} von {{ users.length }} angezeigt</p>
@@ -222,7 +225,7 @@ onMounted(() => {
<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>
<UserAvatar class="admin-users-cell-user__avatar" :initials="userInitials(user)" />
<div>
<p class="admin-users-cell-user__name">{{ user.userName || '(ohne Benutzername)' }}</p>
<p class="admin-users-cell-user__id">{{ user.id }}</p>
@@ -231,24 +234,14 @@ onMounted(() => {
</td>
<td>{{ formatRoles(user.roles) }}</td>
<td>
<span
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
<StatusPill :variant="user.isActive ? 'success' : 'danger'">
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
</StatusPill>
</td>
<td>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
<StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
{{ user.mustChangePassword ? 'Erforderlich' : 'Aktuell' }}
</span>
</StatusPill>
</td>
<td class="admin-users-col-actions">
<v-btn
@@ -268,7 +261,7 @@ onMounted(() => {
<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>
<UserAvatar class="admin-users-mobile-avatar" :initials="userInitials(user)" />
<div>
<p class="admin-users-mobile-label">Benutzer</p>
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
@@ -284,27 +277,17 @@ onMounted(() => {
<div>
<dt>Aktiv</dt>
<dd>
<span
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
<StatusPill :variant="user.isActive ? 'success' : 'danger'">
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span>
</StatusPill>
</dd>
</div>
<div>
<dt>Passwortwechsel</dt>
<dd>
<span
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
<StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</span>
</StatusPill>
</dd>
</div>
</dl>
@@ -325,15 +308,15 @@ onMounted(() => {
<style scoped>
.admin-users-page {
--hoard-page-width: 1200px;
--ui-page-width: 1200px;
}
/* ---------- 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%;
--ui-gradient-angle: 120deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
@@ -505,7 +488,7 @@ onMounted(() => {
white-space: nowrap;
}
.hoard-empty-state {
.ui-empty-state {
display: flex;
flex-direction: column;
align-items: center;
@@ -516,7 +499,7 @@ onMounted(() => {
.admin-users-header,
.admin-users-stat,
.admin-users-listing {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
.admin-users-stat:nth-child(2) { animation-delay: 60ms; }
@@ -98,14 +98,14 @@ async function handleSubmit() {
</script>
<template>
<v-container fluid class="change-password-page hoard-page hoard-page--centered">
<section class="change-password-shell hoard-panel hoard-shell-grid">
<v-container fluid class="change-password-page ui-page ui-page--centered">
<section class="change-password-shell ui-panel ui-shell-grid">
<header class="change-password-head">
<span class="hoard-icon-tile hoard-icon-tile--lg">
<span class="ui-icon-tile ui-icon-tile--lg">
<v-icon icon="mdi-shield-key-outline" size="24" />
</span>
<div>
<p class="hoard-kicker hoard-kicker--xs">Sicherheitsvorgabe</p>
<p class="ui-kicker ui-kicker--xs">Sicherheitsvorgabe</p>
<h1>Passwort ändern</h1>
<p>Aktualisiere dein Hoard-Passwort. Nach der Änderung wirst du erneut zur Anmeldung weitergeleitet.</p>
</div>
@@ -121,7 +121,7 @@ async function handleSubmit() {
<v-form class="change-password-form" @submit.prevent="handleSubmit">
<section class="change-password-section">
<p class="hoard-kicker hoard-kicker--xs hoard-kicker--plain">Aktuell</p>
<p class="ui-kicker ui-kicker--xs ui-kicker--plain">Aktuell</p>
<v-text-field
v-model="oldPassword"
label="Altes Passwort"
@@ -135,10 +135,10 @@ async function handleSubmit() {
/>
</section>
<hr class="hoard-divider-soft" />
<hr class="ui-divider-soft" />
<section class="change-password-section">
<p class="hoard-kicker hoard-kicker--xs hoard-kicker--plain">Neues Passwort</p>
<p class="ui-kicker ui-kicker--xs ui-kicker--plain">Neues Passwort</p>
<v-text-field
v-model="newPassword"
@@ -204,7 +204,7 @@ async function handleSubmit() {
<style scoped>
.change-password-page {
--hoard-shell-width: min(720px, 100%);
--ui-shell-width: min(720px, 100%);
}
.change-password-shell {
@@ -286,7 +286,7 @@ async function handleSubmit() {
@media (prefers-reduced-motion: no-preference) {
.change-password-shell {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
}
+19 -16
View File
@@ -80,15 +80,15 @@ onMounted(() => {
</script>
<template>
<v-container fluid class="login-page hoard-page hoard-page--centered">
<section class="login-shell hoard-panel hoard-panel-gradient hoard-spotlight">
<v-container fluid class="login-page ui-page ui-page--centered">
<section class="login-shell ui-panel ui-panel-gradient ui-spotlight">
<aside class="login-brand">
<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>
<p class="ui-kicker ui-kicker--wide">Willkommen bei Hoard</p>
<h1>
Deine Dateien.<br />
<span class="login-brand__accent">Aufgeräumt.</span>
@@ -100,21 +100,21 @@ onMounted(() => {
<ul class="login-points">
<li>
<span class="hoard-icon-tile"><v-icon icon="mdi-folder-outline" size="18" /></span>
<span class="ui-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>
<span class="hoard-icon-tile"><v-icon icon="mdi-language-markdown-outline" size="18" /></span>
<span class="ui-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>
<span class="hoard-icon-tile"><v-icon icon="mdi-shield-check-outline" size="18" /></span>
<span class="ui-icon-tile"><v-icon icon="mdi-shield-check-outline" size="18" /></span>
<div>
<p class="login-points__title">Self-hosted &amp; sicher</p>
<p class="login-points__text">Cookie-Auth, Rollenmodell, deine Infrastruktur.</p>
@@ -125,7 +125,7 @@ onMounted(() => {
<v-form class="login-form" @submit.prevent="handleSubmit">
<header class="login-form__head">
<p class="hoard-kicker hoard-kicker--xs">Login</p>
<p class="ui-kicker ui-kicker--xs">Login</p>
<h2>Anmelden</h2>
<p>Melde dich mit deinem bestehenden Hoard-Konto an.</p>
</header>
@@ -181,19 +181,21 @@ onMounted(() => {
<style scoped>
.login-page {
--hoard-centered-offset: 200px;
--ui-centered-offset: 200px;
}
.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%;
--ui-shell-width: 1080px;
--ui-shell-padding: 0;
--ui-shell-gap: 0;
--ui-gradient-angle: 120deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 60%;
display: grid;
grid-template-columns: minmax(280px, 1.1fr) minmax(320px, 460px);
align-items: stretch;
border-radius: var(--radius-xl);
overflow: hidden;
}
@@ -356,7 +358,7 @@ h1 {
@media (prefers-reduced-motion: no-preference) {
.login-brand,
.login-form {
animation: hoard-soft-enter 320ms both;
animation: ui-soft-enter 320ms both;
}
.login-form {
@@ -366,6 +368,7 @@ h1 {
@media (width <= 960px) {
.login-shell {
display: block;
grid-template-columns: 1fr;
}
+25 -27
View File
@@ -3,6 +3,8 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { AuthRequestError, fetchCurrentUser, type CurrentUser } from '@/services/authSession'
import StatusPill from '@/components/ui/StatusPill.vue'
import UserAvatar from '@/components/ui/UserAvatar.vue'
const router = useRouter()
const isLoading = ref(true)
@@ -94,10 +96,10 @@ onMounted(() => {
</script>
<template>
<v-container fluid class="dashboard-page hoard-page">
<header class="dashboard-greeting hoard-panel hoard-panel-gradient hoard-spotlight">
<v-container fluid class="dashboard-page ui-page">
<header class="dashboard-greeting ui-panel ui-panel-gradient ui-spotlight">
<div class="dashboard-greeting__copy">
<p class="hoard-kicker hoard-kicker--wide">Geschützter Bereich</p>
<p class="ui-kicker ui-kicker--wide">Geschützter Bereich</p>
<h1>
<template v-if="user">Hallo, {{ user.userName || 'willkommen' }}.</template>
<template v-else>Dashboard</template>
@@ -108,18 +110,18 @@ onMounted(() => {
</p>
<div class="dashboard-greeting__chips">
<span v-for="role in roleChips" :key="role" class="hoard-chip hoard-chip--brand">
<span v-for="role in roleChips" :key="role" class="ui-chip ui-chip--brand">
<v-icon icon="mdi-shield-account-outline" size="14" />
{{ role }}
</span>
<span v-if="roleChips.length === 0" class="hoard-chip">
<span v-if="roleChips.length === 0" class="ui-chip">
<v-icon icon="mdi-account-outline" size="14" /> Keine Rollen
</span>
</div>
</div>
<div v-if="user" class="dashboard-greeting__avatar">
<span class="dashboard-avatar">{{ userInitials }}</span>
<UserAvatar class="dashboard-avatar" :initials="userInitials" />
<p class="dashboard-avatar__caption">{{ user.userName }}</p>
</div>
</header>
@@ -133,9 +135,9 @@ onMounted(() => {
</v-alert>
<section v-else class="dashboard-grid">
<article class="dashboard-stat hoard-panel">
<article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-account-outline" size="20" />
</span>
<p class="dashboard-stat__label">Konto</p>
@@ -144,9 +146,9 @@ onMounted(() => {
<p class="dashboard-stat__hint">Angemeldet als interner Benutzer</p>
</article>
<article class="dashboard-stat hoard-panel">
<article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head">
<span class="hoard-icon-tile">
<span class="ui-icon-tile">
<v-icon icon="mdi-shield-account-outline" size="20" />
</span>
<p class="dashboard-stat__label">Rollen</p>
@@ -155,34 +157,30 @@ onMounted(() => {
<p class="dashboard-stat__hint">Definiert deine sichtbaren Bereiche</p>
</article>
<article class="dashboard-stat hoard-panel">
<article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head">
<span class="hoard-icon-tile">
<span class="ui-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>
<StatusPill :variant="accountStatusVariant">{{ accountStatusLabel }}</StatusPill>
<StatusPill :variant="passwordStatusVariant">{{ passwordStatusLabel }}</StatusPill>
</div>
<p class="dashboard-stat__hint">Konto- und Passwortzustand</p>
</article>
</section>
<section v-if="!errorMessage" class="dashboard-detail hoard-panel">
<section v-if="!errorMessage" class="dashboard-detail ui-panel">
<header class="dashboard-detail__head">
<div>
<p class="hoard-kicker">Auth-Antwort</p>
<p class="ui-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">
<div class="dashboard-detail__actions ui-action-row">
<v-btn
variant="outlined"
prepend-icon="mdi-lock-reset"
@@ -210,15 +208,15 @@ onMounted(() => {
<style scoped>
.dashboard-page {
--hoard-page-width: 1120px;
--ui-page-width: 1120px;
}
/* ---------- 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%;
--ui-gradient-angle: 120deg;
--ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--ui-gradient-end: var(--color-surface);
--ui-gradient-end-stop: 65%;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -394,7 +392,7 @@ h1 {
.dashboard-greeting,
.dashboard-stat,
.dashboard-detail {
animation: hoard-soft-enter 280ms both;
animation: ui-soft-enter 280ms both;
}
.dashboard-stat:nth-child(2) {
+42 -42
View File
@@ -2,54 +2,54 @@
Hoard Page-Layout-Primitives
============================================================================= */
.hoard-page {
.ui-page {
display: flex;
flex-direction: column;
gap: var(--hoard-page-gap, var(--space-6));
gap: var(--ui-page-gap, var(--space-6));
margin-inline: auto;
width: min(100%, var(--hoard-page-width, 1180px));
width: min(100%, var(--ui-page-width, 1180px));
padding-block:
var(--hoard-page-padding-start, var(--space-5))
var(--hoard-page-padding-end, var(--space-12));
var(--ui-page-padding-start, var(--space-5))
var(--ui-page-padding-end, var(--space-12));
}
.hoard-page--centered {
.ui-page--centered {
width: 100%;
margin-inline: 0;
align-items: center;
justify-content: center;
min-height: calc(100vh - var(--hoard-centered-offset, 220px));
padding: var(--hoard-centered-padding, var(--space-10) var(--space-4));
min-height: calc(100vh - var(--ui-centered-offset, 220px));
padding: var(--ui-centered-padding, var(--space-10) var(--space-4));
}
.hoard-shell-grid {
.ui-shell-grid {
display: grid;
gap: var(--hoard-shell-gap, var(--space-8));
width: min(100%, var(--hoard-shell-width, 1080px));
padding: var(--hoard-shell-padding, var(--space-8));
gap: var(--ui-shell-gap, var(--space-8));
width: min(100%, var(--ui-shell-width, 1080px));
padding: var(--ui-shell-padding, var(--space-8));
}
.hoard-page-header {
.ui-page-header {
display: flex;
flex-direction: column;
gap: var(--space-2);
max-width: 78ch;
}
.hoard-page-header > h1 {
.ui-page-header > h1 {
margin: 0;
font-size: var(--font-size-3xl);
letter-spacing: -0.02em;
}
.hoard-page-header > p {
.ui-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 {
.ui-page-header__meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
@@ -57,57 +57,57 @@
}
@media (width <= 960px) {
.hoard-page {
.ui-page {
width: 100%;
gap: var(--hoard-page-gap-mobile, var(--space-5));
gap: var(--ui-page-gap-mobile, var(--space-5));
padding-inline:
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)));
var(--ui-page-padding-inline-start-mobile, max(var(--space-3), env(safe-area-inset-left)))
var(--ui-page-padding-inline-end-mobile, max(var(--space-3), env(safe-area-inset-right)));
padding-block:
var(--hoard-page-padding-start-mobile, var(--space-3))
var(--hoard-page-padding-end-mobile, var(--space-8));
var(--ui-page-padding-start-mobile, var(--space-3))
var(--ui-page-padding-end-mobile, var(--space-8));
}
.hoard-page--centered {
.ui-page--centered {
width: 100%;
min-height: calc(100vh - var(--hoard-centered-offset-mobile, 200px));
padding: var(--hoard-centered-padding-mobile, var(--space-6) var(--space-3));
min-height: calc(100vh - var(--ui-centered-offset-mobile, 200px));
padding: var(--ui-centered-padding-mobile, var(--space-6) var(--space-3));
}
.hoard-shell-grid {
.ui-shell-grid {
width: 100%;
gap: var(--hoard-shell-gap-mobile, var(--space-5));
gap: var(--ui-shell-gap-mobile, var(--space-5));
padding:
var(--hoard-shell-padding-block-mobile, var(--space-6))
var(--hoard-shell-padding-inline-mobile, var(--space-5));
var(--ui-shell-padding-block-mobile, var(--space-6))
var(--ui-shell-padding-inline-mobile, var(--space-5));
}
.hoard-page-header > h1 {
.ui-page-header > h1 {
font-size: var(--font-size-2xl);
}
.hoard-page-header > p {
.ui-page-header > p {
font-size: var(--font-size-md);
}
}
@media (width <= 600px) {
.hoard-page {
gap: var(--hoard-page-gap-mobile-xs, var(--space-4));
.ui-page {
gap: var(--ui-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-6));
var(--ui-page-padding-start-mobile-xs, var(--space-2))
var(--ui-page-padding-end-mobile-xs, var(--space-6));
}
.hoard-page--centered {
min-height: calc(100vh - var(--hoard-centered-offset-mobile-xs, 180px));
padding: var(--hoard-centered-padding-mobile-xs, var(--space-5) var(--space-3));
.ui-page--centered {
min-height: calc(100vh - var(--ui-centered-offset-mobile-xs, 180px));
padding: var(--ui-centered-padding-mobile-xs, var(--space-5) var(--space-3));
}
.hoard-shell-grid {
gap: var(--hoard-shell-gap-mobile-xs, var(--space-4));
.ui-shell-grid {
gap: var(--ui-shell-gap-mobile-xs, var(--space-4));
padding:
var(--hoard-shell-padding-block-mobile-xs, var(--space-5))
var(--hoard-shell-padding-inline-mobile-xs, var(--space-4));
var(--ui-shell-padding-block-mobile-xs, var(--space-5))
var(--ui-shell-padding-inline-mobile-xs, var(--space-4));
}
}
+36 -36
View File
@@ -2,7 +2,7 @@
Hoard Surface- und Inhaltspattern
============================================================================= */
.hoard-kicker {
.ui-kicker {
display: inline-flex;
align-items: center;
gap: var(--space-2);
@@ -14,7 +14,7 @@
text-transform: uppercase;
}
.hoard-kicker::before {
.ui-kicker::before {
content: '';
width: 18px;
height: 1px;
@@ -22,32 +22,32 @@
opacity: 0.6;
}
.hoard-kicker--wide {
.ui-kicker--wide {
letter-spacing: 0.12em;
}
.hoard-kicker--xs {
.ui-kicker--xs {
margin-bottom: var(--space-2);
font-size: var(--font-size-2xs);
letter-spacing: 0.1em;
}
.hoard-kicker--plain::before {
.ui-kicker--plain::before {
display: none;
}
.hoard-action-row {
.ui-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
.hoard-action-row--end {
.ui-action-row--end {
justify-content: flex-end;
}
.hoard-panel-gradient {
.ui-panel-gradient {
background:
radial-gradient(
120% 80% at 100% 0%,
@@ -55,21 +55,21 @@
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%))
var(--ui-gradient-angle, 130deg),
var(--ui-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%)
var(--ui-gradient-end, var(--color-surface)) var(--ui-gradient-end-stop, 60%)
);
}
.hoard-spotlight {
.ui-spotlight {
position: relative;
overflow: hidden;
isolation: isolate;
}
.hoard-spotlight::before,
.hoard-spotlight::after {
.ui-spotlight::before,
.ui-spotlight::after {
content: '';
position: absolute;
z-index: -1;
@@ -78,7 +78,7 @@
filter: blur(60px);
}
.hoard-spotlight::before {
.ui-spotlight::before {
inset: -10% auto auto -10%;
width: 360px;
height: 360px;
@@ -86,7 +86,7 @@
opacity: 0.55;
}
.hoard-spotlight::after {
.ui-spotlight::after {
inset: auto -10% -10% auto;
width: 320px;
height: 320px;
@@ -94,17 +94,17 @@
opacity: 0.35;
}
[data-theme='dark'] .hoard-spotlight::before {
[data-theme='dark'] .ui-spotlight::before {
background: color-mix(in srgb, var(--color-primary-700) 50%, transparent);
opacity: 0.45;
}
[data-theme='dark'] .hoard-spotlight::after {
[data-theme='dark'] .ui-spotlight::after {
background: color-mix(in srgb, var(--color-primary-600) 30%, transparent);
opacity: 0.4;
}
.hoard-chip {
.ui-chip {
display: inline-flex;
align-items: center;
gap: var(--space-2);
@@ -119,23 +119,23 @@
white-space: nowrap;
}
.hoard-chip--brand {
.ui-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 {
.ui-chip--ghost {
border-color: var(--color-border-subtle);
background-color: transparent;
}
.hoard-chip > .v-icon {
.ui-chip > .v-icon {
width: 14px;
height: 14px;
}
.hoard-icon-tile {
.ui-icon-tile {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -153,19 +153,19 @@
flex: 0 0 auto;
}
.hoard-icon-tile--lg {
.ui-icon-tile--lg {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
}
.hoard-icon-tile--ghost {
.ui-icon-tile--ghost {
color: var(--color-text-secondary);
background: var(--color-surface);
border-color: var(--color-border);
}
.hoard-divider-soft {
.ui-divider-soft {
border: 0;
height: 1px;
background:
@@ -179,7 +179,7 @@
);
}
.hoard-section-head {
.ui-section-head {
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -187,24 +187,24 @@
max-width: 70ch;
}
.hoard-section-head > h2 {
.ui-section-head > h2 {
margin: 0;
font-size: var(--font-size-2xl);
letter-spacing: -0.015em;
}
.hoard-section-head > p {
.ui-section-head > p {
margin: 0;
color: var(--color-text-secondary);
}
@media (width <= 960px) {
.hoard-action-row {
.ui-action-row {
gap: var(--space-2);
}
.hoard-spotlight::before,
.hoard-spotlight::after {
.ui-spotlight::before,
.ui-spotlight::after {
width: 240px;
height: 240px;
filter: blur(48px);
@@ -212,21 +212,21 @@
}
@media (width <= 600px) {
.hoard-kicker {
.ui-kicker {
margin-bottom: var(--space-2);
}
.hoard-action-row {
.ui-action-row {
display: grid;
grid-template-columns: 1fr;
width: 100%;
}
.hoard-action-row > * {
.ui-action-row > * {
width: 100%;
}
.hoard-section-head > h2 {
.ui-section-head > h2 {
font-size: var(--font-size-xl);
}
}
+6 -6
View File
@@ -126,7 +126,7 @@ Modulare Skala in 4er-Schritten:
--space-16: 64px
```
Hauptseiten: `--hoard-page-width` 11201200px, Padding orientiert sich an `--space-6`/`--space-8`.
Hauptseiten: `--ui-page-width` 11201200px, Padding orientiert sich an `--space-6`/`--space-8`.
## Formensprache (Border-Radien)
- `--radius-xs`: 6px kleine Pills, Status-Badges.
@@ -214,9 +214,9 @@ Regeln:
- 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.
- `ui-list-row` (Datei-/Item-Zeilen): luftiges Padding, klare Spalten, `transform: translateX(2px)` beim Hover.
### Status-Pills (`hoard-status`)
### Status-Pills (`ui-status`)
- Kompakt, `--radius-xs`, mit dezentem Text/Background-Tint pro Status (Success/Info/Warning/Danger/Neutral).
- Optional kleines Punktindikator-Dot vor dem Text.
@@ -227,11 +227,11 @@ Regeln:
- Enter/Leave: 180 ms Fade + 10 px Y.
## Hero-/Marketing-Bereiche
- Erlauben einen **ambient Verlauf** (`hoard-panel-gradient` + Variablen).
- Erlauben einen **ambient Verlauf** (`ui-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.
- Hero-Tags (`ui-chip`/`ui-tag`) als kompakte, dezent gerahmte Pills.
## Vorschau-Bereich (Files)
- **PDF-Vorschau:** neutraler Hintergrund, weiße „Papier"-Fläche mit `--shadow-md`, genug Rand. Controls minimal.
@@ -268,7 +268,7 @@ Desktop ist Hauptfokus, Mobile muss aber sauber funktionieren.
### Immer
- Tokens nutzen (Farben, Spacing, Radius, Shadow).
- Patterns wiederverwenden (`hoard-panel`, `hoard-page`, `hoard-action-row`, `hoard-kicker`, `hoard-status`, `hoard-chip`, `hoard-spotlight`).
- Patterns wiederverwenden (`ui-panel`, `ui-page`, `ui-action-row`, `ui-kicker`, `ui-status`, `ui-chip`, `ui-spotlight`).
- Light- und Dark-Mode gleichwertig prüfen.
- Animationen kurz halten und `prefers-reduced-motion` respektieren.