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. - 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 ## 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/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 (`hoard-page`, `hoard-page--centered`, `hoard-shell-grid`). - `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 (`hoard-kicker`, `hoard-action-row`, `hoard-panel-gradient`, `hoard-chip`, `hoard-spotlight` …). - `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. - Alle drei werden in `GUI/src/main.ts` einmalig importiert.
- Neue Layouts/Patterns immer **zuerst** dort ergänzen, nicht in `scoped`-Styles duplizieren. - Neue Layouts/Patterns immer **zuerst** dort ergänzen, nicht in `scoped`-Styles duplizieren.
## Anleitung: CSS-Patterns verwenden ## Anleitung: CSS-Patterns verwenden
- Neue Seiten standardmäßig mit `hoard-page` aufbauen; für zentrierte Vollhöhen-Ansichten zusätzlich `hoard-page--centered`. - Neue Seiten standardmäßig mit `ui-page` aufbauen; für zentrierte Vollhöhen-Ansichten zusätzlich `ui-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`). - 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 `hoard-kicker` (+ ggf. `--wide`/`--xs`). - Wiederkehrende Headlines/Kicker mit `ui-kicker` (+ ggf. `--wide`/`--xs`).
- Aktionszeilen mit `hoard-action-row` bauen statt eigene Flex-Definitionen pro Seite. - Aktionszeilen mit `ui-action-row` bauen statt eigene Flex-Definitionen pro Seite.
- Gradient-Flächen über `hoard-panel-gradient` + Variablen steuern. - Gradient-Flächen über `ui-panel-gradient` + Variablen steuern.
- Lokales `scoped` CSS nur für wirklich seitenspezifische Sonderfälle. - Lokales `scoped` CSS nur für wirklich seitenspezifische Sonderfälle.
## Aktueller Stand (Identity-Branch) ## Aktueller Stand (Identity-Branch)
+24 -44
View File
@@ -176,8 +176,8 @@ watch(
</script> </script>
<template> <template>
<v-app class="hoard-shell"> <v-app class="ui-shell">
<v-app-bar class="hoard-app-bar" elevation="0" height="68"> <v-app-bar class="ui-app-bar" elevation="0" height="68">
<template #prepend> <template #prepend>
<v-app-bar-nav-icon <v-app-bar-nav-icon
v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'" v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'"
@@ -314,7 +314,7 @@ watch(
<v-navigation-drawer <v-navigation-drawer
v-model="showDrawer" 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'" :location="display.mobile.value ? 'bottom' : 'left'"
:temporary="display.mobile.value" :temporary="display.mobile.value"
:permanent="!display.mobile.value" :permanent="!display.mobile.value"
@@ -322,7 +322,7 @@ watch(
:elevation="display.mobile.value ? 6 : 0" :elevation="display.mobile.value ? 6 : 0"
> >
<div class="drawer-top"> <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> </div>
<v-list nav :density="display.mobile.value ? 'default' : 'comfortable'" class="px-1"> <v-list nav :density="display.mobile.value ? 'default' : 'comfortable'" class="px-1">
@@ -333,15 +333,15 @@ watch(
:active="route.path === item.path" :active="route.path === item.path"
:prepend-icon="item.icon" :prepend-icon="item.icon"
:title="item.name" :title="item.name"
class="hoard-nav-item" class="ui-nav-item"
rounded="md" rounded="md"
link link
/> />
<template v-if="adminSidebarRoutes.length > 0"> <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"> <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> </div>
<v-list-item <v-list-item
@@ -351,7 +351,7 @@ watch(
:active="route.path === item.path" :active="route.path === item.path"
:prepend-icon="item.icon" :prepend-icon="item.icon"
:title="item.name" :title="item.name"
class="hoard-nav-item" class="ui-nav-item"
rounded="md" rounded="md"
link link
/> />
@@ -359,28 +359,8 @@ watch(
</v-list> </v-list>
<template #append> <template #append>
<div class="drawer-bottom"> <div v-if="!isAuthenticated" 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-btn
v-else
variant="text" variant="text"
prepend-icon="mdi-home-outline" prepend-icon="mdi-home-outline"
to="/welcome" to="/welcome"
@@ -392,7 +372,7 @@ watch(
</template> </template>
</v-navigation-drawer> </v-navigation-drawer>
<v-main class="hoard-main"> <v-main class="ui-main">
<div class="main-shell"> <div class="main-shell">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="route-fade" mode="out-in"> <transition name="route-fade" mode="out-in">
@@ -403,7 +383,7 @@ watch(
<v-footer <v-footer
v-if="shouldShowFooter" v-if="shouldShowFooter"
class="hoard-footer" class="ui-footer"
> >
<div class="footer-inner"> <div class="footer-inner">
<div class="footer-brand"> <div class="footer-brand">
@@ -455,11 +435,11 @@ watch(
</template> </template>
<style scoped> <style scoped>
.hoard-shell { .ui-shell {
min-height: 100vh; min-height: 100vh;
} }
.hoard-app-bar { .ui-app-bar {
padding-inline: var(--space-3); padding-inline: var(--space-3);
} }
@@ -676,11 +656,11 @@ watch(
} }
/* ---------- Drawer ---------- */ /* ---------- Drawer ---------- */
.hoard-drawer { .ui-drawer {
padding-top: var(--space-2); padding-top: var(--space-2);
} }
.hoard-drawer--mobile { .ui-drawer--mobile {
border-top-left-radius: var(--radius-xl); border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl); border-top-right-radius: var(--radius-xl);
max-height: min(74vh, 580px); max-height: min(74vh, 580px);
@@ -702,11 +682,11 @@ watch(
padding: 0 var(--space-5) var(--space-2); padding: 0 var(--space-5) var(--space-2);
} }
:deep(.hoard-nav-item) { :deep(.ui-nav-item) {
min-height: 44px; min-height: 44px;
} }
:deep(.hoard-nav-item .v-list-item-title) { :deep(.ui-nav-item .v-list-item-title) {
font-weight: 500; font-weight: 500;
} }
@@ -768,7 +748,7 @@ watch(
} }
/* ---------- Main ---------- */ /* ---------- Main ---------- */
.hoard-main { .ui-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: calc(100vh - 68px); min-height: calc(100vh - 68px);
@@ -797,7 +777,7 @@ watch(
} }
/* ---------- Footer ---------- */ /* ---------- Footer ---------- */
.hoard-footer { .ui-footer {
flex: 0 0 auto; flex: 0 0 auto;
height: auto !important; height: auto !important;
min-height: 0 !important; min-height: 0 !important;
@@ -927,7 +907,7 @@ watch(
/* ---------- Responsive ---------- */ /* ---------- Responsive ---------- */
@media (width <= 960px) { @media (width <= 960px) {
.hoard-app-bar { .ui-app-bar {
padding-inline: padding-inline:
max(var(--space-1), env(safe-area-inset-left)) max(var(--space-1), env(safe-area-inset-left))
max(var(--space-1), env(safe-area-inset-right)); max(var(--space-1), env(safe-area-inset-right));
@@ -962,7 +942,7 @@ watch(
display: none; display: none;
} }
.hoard-drawer { .ui-drawer {
padding-top: var(--space-1); padding-top: var(--space-1);
} }
@@ -995,9 +975,9 @@ watch(
} }
.app-banner-stack { .app-banner-stack {
right: var(--hoard-mobile-safe-right); right: var(--ui-mobile-safe-right);
left: var(--hoard-mobile-safe-left); left: var(--ui-mobile-safe-left);
bottom: calc(var(--hoard-mobile-safe-bottom) + var(--space-1)); bottom: calc(var(--ui-mobile-safe-bottom) + var(--space-1));
} }
.app-banner-list { .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 ---------- */ /* ---------- Status pills ---------- */
.hoard-status { .ui-status {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
@@ -518,7 +518,7 @@ pre {
white-space: nowrap; white-space: nowrap;
} }
.hoard-status::before { .ui-status::before {
content: ''; content: '';
width: 6px; width: 6px;
height: 6px; height: 6px;
@@ -527,37 +527,37 @@ pre {
box-shadow: 0 0 0 3px color-mix(in srgb, currentcolor 22%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, currentcolor 22%, transparent);
} }
.hoard-status--success { .ui-status--success {
color: var(--color-success); color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 14%, var(--color-surface) 86%); background-color: color-mix(in srgb, var(--color-success) 14%, var(--color-surface) 86%);
} }
.hoard-status--warning { .ui-status--warning {
color: var(--color-warning); color: var(--color-warning);
background-color: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface) 84%); background-color: color-mix(in srgb, var(--color-warning) 16%, var(--color-surface) 84%);
} }
.hoard-status--danger { .ui-status--danger {
color: var(--color-danger); color: var(--color-danger);
background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface) 86%); background-color: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface) 86%);
} }
.hoard-status--info { .ui-status--info {
color: var(--color-info); color: var(--color-info);
background-color: color-mix(in srgb, var(--color-info) 14%, var(--color-surface) 86%); 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); color: var(--color-text-muted);
background-color: color-mix(in srgb, var(--color-surface-alt) 70%, var(--color-surface) 30%); 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); background-color: var(--color-text-muted);
} }
/* ---------- Reusable surfaces ---------- */ /* ---------- Reusable surfaces ---------- */
.hoard-panel { .ui-panel {
position: relative; position: relative;
background: background:
linear-gradient( linear-gradient(
@@ -574,19 +574,19 @@ pre {
transform var(--transition-fast); transform var(--transition-fast);
} }
.hoard-panel--elevated { .ui-panel--elevated {
background-color: var(--color-surface-elevated); background-color: var(--color-surface-elevated);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
.hoard-panel--ghost { .ui-panel--ghost {
background: background:
color-mix(in srgb, var(--color-surface-alt) 60%, var(--color-surface) 40%); color-mix(in srgb, var(--color-surface-alt) 60%, var(--color-surface) 40%);
border-color: var(--color-border-subtle); border-color: var(--color-border-subtle);
box-shadow: none; box-shadow: none;
} }
.hoard-toolbar { .ui-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -598,7 +598,7 @@ pre {
border-top-right-radius: inherit; border-top-right-radius: inherit;
} }
.hoard-list-row { .ui-list-row {
display: grid; display: grid;
grid-template-columns: minmax(220px, 2fr) minmax(120px, 1fr) minmax(100px, 1fr) minmax(120px, 1fr); grid-template-columns: minmax(220px, 2fr) minmax(120px, 1fr) minmax(100px, 1fr) minmax(120px, 1fr);
align-items: center; align-items: center;
@@ -610,7 +610,7 @@ pre {
transform var(--transition-fast); transform var(--transition-fast);
} }
.hoard-list-row:hover { .ui-list-row:hover {
background-color: color-mix( background-color: color-mix(
in srgb, in srgb,
var(--color-primary-100) 32%, var(--color-primary-100) 32%,
@@ -619,7 +619,7 @@ pre {
transform: translateX(2px); transform: translateX(2px);
} }
.hoard-list-row.is-selected { .ui-list-row.is-selected {
color: var(--color-primary-700); color: var(--color-primary-700);
background-color: color-mix( background-color: color-mix(
in srgb, in srgb,
@@ -628,24 +628,24 @@ pre {
); );
} }
.hoard-meta { .ui-meta {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
.hoard-empty-state { .ui-empty-state {
padding: var(--space-10) var(--space-6); padding: var(--space-10) var(--space-6);
text-align: center; text-align: center;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.hoard-empty-state h2 { .ui-empty-state h2 {
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
font-weight: 600; font-weight: 600;
} }
.hoard-empty-state p { .ui-empty-state p {
margin: 0 auto; margin: 0 auto;
max-width: 44ch; max-width: 44ch;
} }
@@ -678,7 +678,7 @@ pre {
} }
/* ---------- Animations ---------- */ /* ---------- Animations ---------- */
@keyframes hoard-soft-enter { @keyframes ui-soft-enter {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(8px);
@@ -690,7 +690,7 @@ pre {
} }
} }
@keyframes hoard-pulse-ring { @keyframes ui-pulse-ring {
0% { 0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary-300) 35%, transparent); box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary-300) 35%, transparent);
} }
@@ -716,7 +716,7 @@ pre {
.v-btn, .v-btn,
.v-navigation-drawer .v-list-item, .v-navigation-drawer .v-list-item,
.hoard-list-row { .ui-list-row {
transform: none !important; transform: none !important;
} }
} }
@@ -724,13 +724,13 @@ pre {
/* ---------- Responsive ---------- */ /* ---------- Responsive ---------- */
@media (width <= 960px) { @media (width <= 960px) {
:root { :root {
--hoard-mobile-safe-left: max(var(--space-2), env(safe-area-inset-left)); --ui-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)); --ui-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-bottom: max(var(--space-3), env(safe-area-inset-bottom));
} }
.v-main { .v-main {
padding-bottom: var(--hoard-mobile-safe-bottom); padding-bottom: var(--ui-mobile-safe-bottom);
} }
.v-btn { .v-btn {
@@ -747,40 +747,40 @@ pre {
margin: 4px var(--space-2); margin: 4px var(--space-2);
} }
.hoard-list-row { .ui-list-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--space-2); gap: var(--space-2);
align-items: start; align-items: start;
padding-block: var(--space-4); padding-block: var(--space-4);
} }
.hoard-toolbar { .ui-toolbar {
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
padding: var(--space-3); padding: var(--space-3);
} }
.hoard-empty-state { .ui-empty-state {
padding: var(--space-8) var(--space-4); padding: var(--space-8) var(--space-4);
} }
.v-navigation-drawer { .v-navigation-drawer {
border-right: none !important; border-right: none !important;
border-top: 1px solid var(--color-border) !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) { @media (width <= 600px) {
.hoard-toolbar { .ui-toolbar {
gap: var(--space-2); gap: var(--space-2);
} }
.hoard-list-row { .ui-list-row {
padding-inline: var(--space-3); padding-inline: var(--space-3);
} }
.hoard-meta { .ui-meta {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
} }
+10 -10
View File
@@ -72,8 +72,8 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<v-container fluid class="not-found-page hoard-page hoard-page--centered"> <v-container fluid class="not-found-page ui-page ui-page--centered">
<section class="not-found-shell hoard-panel hoard-panel-gradient hoard-spotlight hoard-shell-grid"> <section class="not-found-shell ui-panel ui-panel-gradient ui-spotlight ui-shell-grid">
<div class="not-found-visual"> <div class="not-found-visual">
<div class="image-frame"> <div class="image-frame">
<img <img
@@ -86,14 +86,14 @@ onBeforeUnmount(() => {
<div class="not-found-content"> <div class="not-found-content">
<p class="not-found-code">404</p> <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> <h1>Diese Spur führt ins Leere.</h1>
<p class="not-found-text"> <p class="not-found-text">
Der Link ist ungültig oder die Seite wurde verschoben. Wir leiten dich gleich zur 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. {{ autoRedirectTargetLabel }} weiter oder du nutzt direkt einen der Buttons unten.
</p> </p>
<div class="not-found-actions hoard-action-row"> <div class="not-found-actions ui-action-row">
<v-btn <v-btn
variant="elevated" variant="elevated"
:prepend-icon="redirectButtonIcon" :prepend-icon="redirectButtonIcon"
@@ -117,11 +117,11 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.not-found-shell { .not-found-shell {
--hoard-shell-width: 1040px; --ui-shell-width: 1040px;
--hoard-gradient-angle: 130deg; --ui-gradient-angle: 130deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr); grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
@@ -211,7 +211,7 @@ h1 {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.not-found-visual, .not-found-visual,
.not-found-content { .not-found-content {
animation: hoard-soft-enter 320ms both; animation: ui-soft-enter 320ms both;
} }
.not-found-content { .not-found-content {
+10 -10
View File
@@ -47,15 +47,15 @@ onMounted(() => {
</script> </script>
<template> <template>
<v-container fluid class="forbidden-page hoard-page hoard-page--centered"> <v-container fluid class="forbidden-page ui-page ui-page--centered">
<section class="forbidden-shell hoard-panel hoard-panel-gradient hoard-spotlight"> <section class="forbidden-shell ui-panel ui-panel-gradient ui-spotlight">
<div class="forbidden-icon"> <div class="forbidden-icon">
<span class="forbidden-icon__halo" aria-hidden="true" /> <span class="forbidden-icon__halo" aria-hidden="true" />
<v-icon icon="mdi-shield-alert-outline" size="40" /> <v-icon icon="mdi-shield-alert-outline" size="40" />
</div> </div>
<header class="forbidden-head"> <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> <h1>Kein Zugriff.</h1>
<p> <p>
Dein Konto hat aktuell keine ausreichende Rolle, um diese Seite zu sehen. Falls das ein Fehler ist, Dein Konto hat aktuell keine ausreichende Rolle, um diese Seite zu sehen. Falls das ein Fehler ist,
@@ -63,7 +63,7 @@ onMounted(() => {
</p> </p>
</header> </header>
<div class="forbidden-actions hoard-action-row"> <div class="forbidden-actions ui-action-row">
<v-btn <v-btn
variant="elevated" variant="elevated"
:prepend-icon="primaryActionIcon" :prepend-icon="primaryActionIcon"
@@ -83,14 +83,14 @@ onMounted(() => {
<style scoped> <style scoped>
.forbidden-page { .forbidden-page {
--hoard-centered-offset: 200px; --ui-centered-offset: 200px;
} }
.forbidden-shell { .forbidden-shell {
--hoard-gradient-angle: 130deg; --ui-gradient-angle: 130deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface) 86%); --ui-gradient-start: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface) 86%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -152,7 +152,7 @@ onMounted(() => {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.forbidden-shell { .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> </script>
<template> <template>
<v-container fluid class="landing-page hoard-page"> <v-container fluid class="landing-page ui-page">
<section class="hero hoard-panel hoard-panel-gradient hoard-spotlight"> <section class="hero ui-panel ui-panel-gradient ui-spotlight">
<div class="hero-copy"> <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> <h1>
Eine ruhige Heimat für deine Eine ruhige Heimat für deine
<span class="hero-accent">Dateien, Ordner und Notizen.</span> <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. Daten, Struktur und Workflow behalten wollen ohne Cloud-Lock-in, ohne SaaS-Abo.
</p> </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"> <v-btn variant="elevated" size="large" prepend-icon="mdi-login" to="/login">
Anmelden Anmelden
</v-btn> </v-btn>
@@ -94,13 +94,13 @@ const techStack = [
</div> </div>
<div class="hero-tags"> <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 <v-icon icon="mdi-shield-check-outline" size="14" /> Datenhoheit
</span> </span>
<span class="hoard-chip"> <span class="ui-chip">
<v-icon icon="mdi-account-multiple-outline" size="14" /> Mehrbenutzerfähig <v-icon icon="mdi-account-multiple-outline" size="14" /> Mehrbenutzerfähig
</span> </span>
<span class="hoard-chip"> <span class="ui-chip">
<v-icon icon="mdi-weather-night" size="14" /> Light- und Dark-Mode <v-icon icon="mdi-weather-night" size="14" /> Light- und Dark-Mode
</span> </span>
</div> </div>
@@ -120,7 +120,7 @@ const techStack = [
<div class="preview-window__body"> <div class="preview-window__body">
<header class="preview-toolbar"> <header class="preview-toolbar">
<span class="preview-toolbar__title">Dokumentation</span> <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 <v-icon icon="mdi-folder-outline" size="14" /> 3 Ordner · 12 Dateien
</span> </span>
</header> </header>
@@ -134,7 +134,7 @@ const techStack = [
<p class="preview-row__title">Konzepte</p> <p class="preview-row__title">Konzepte</p>
<p class="preview-row__meta">Ordner · vor 2 Tagen</p> <p class="preview-row__meta">Ordner · vor 2 Tagen</p>
</div> </div>
<span class="hoard-status hoard-status--muted">Geteilt</span> <span class="ui-status ui-status--muted">Geteilt</span>
</article> </article>
<article class="preview-row preview-row--active"> <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__title">roadmap.md</p>
<p class="preview-row__meta">Markdown · 18 KB</p> <p class="preview-row__meta">Markdown · 18 KB</p>
</div> </div>
<span class="hoard-status hoard-status--success">Editor</span> <span class="ui-status ui-status--success">Editor</span>
</article> </article>
<article class="preview-row"> <article class="preview-row">
@@ -156,7 +156,7 @@ const techStack = [
<p class="preview-row__title">api-reference.pdf</p> <p class="preview-row__title">api-reference.pdf</p>
<p class="preview-row__meta">PDF · 1,2 MB</p> <p class="preview-row__meta">PDF · 1,2 MB</p>
</div> </div>
<span class="hoard-status hoard-status--info">Vorschau</span> <span class="ui-status ui-status--info">Vorschau</span>
</article> </article>
<article class="preview-row"> <article class="preview-row">
@@ -167,7 +167,7 @@ const techStack = [
<p class="preview-row__title">screen-2026-04.png</p> <p class="preview-row__title">screen-2026-04.png</p>
<p class="preview-row__meta">Bild · 480 KB</p> <p class="preview-row__meta">Bild · 480 KB</p>
</div> </div>
<span class="hoard-status hoard-status--muted">Bereit</span> <span class="ui-status ui-status--muted">Bereit</span>
</article> </article>
</div> </div>
</div> </div>
@@ -180,8 +180,8 @@ const techStack = [
</section> </section>
<section class="value-grid"> <section class="value-grid">
<article v-for="item in valueProps" :key="item.title" class="value-card hoard-panel"> <article v-for="item in valueProps" :key="item.title" class="value-card ui-panel">
<span class="hoard-icon-tile hoard-icon-tile--lg"> <span class="ui-icon-tile ui-icon-tile--lg">
<v-icon :icon="item.icon" size="22" /> <v-icon :icon="item.icon" size="22" />
</span> </span>
<h2>{{ item.title }}</h2> <h2>{{ item.title }}</h2>
@@ -189,16 +189,16 @@ const techStack = [
</article> </article>
</section> </section>
<section class="feature-section hoard-panel"> <section class="feature-section ui-panel">
<header class="hoard-section-head"> <header class="ui-section-head">
<p class="hoard-kicker">Für den Produktivalltag</p> <p class="ui-kicker">Für den Produktivalltag</p>
<h2>Weniger Tool-Chaos, mehr Fokus auf Inhalte.</h2> <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> <p>Hoard bündelt Datei- und Markdown-Workflows in einer ruhigen Oberfläche, die nicht ablenkt.</p>
</header> </header>
<div class="feature-grid"> <div class="feature-grid">
<article v-for="feature in coreFeatures" :key="feature.title" class="feature-card"> <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" /> <v-icon :icon="feature.icon" size="20" />
</span> </span>
<div> <div>
@@ -210,12 +210,12 @@ const techStack = [
</section> </section>
<section class="workflow-section"> <section class="workflow-section">
<header class="hoard-section-head"> <header class="ui-section-head">
<p class="hoard-kicker">So funktioniert Hoard</p> <p class="ui-kicker">So funktioniert Hoard</p>
<h2>In drei klaren Schritten produktiv starten.</h2> <h2>In drei klaren Schritten produktiv starten.</h2>
</header> </header>
<div class="workflow-grid"> <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> <p class="workflow-number">{{ step.number }}</p>
<h3>{{ step.title }}</h3> <h3>{{ step.title }}</h3>
<p>{{ step.text }}</p> <p>{{ step.text }}</p>
@@ -223,9 +223,9 @@ const techStack = [
</div> </div>
</section> </section>
<section class="stack-section hoard-panel hoard-panel-gradient"> <section class="stack-section ui-panel ui-panel-gradient">
<div class="stack-copy"> <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> <h2>Schlank gebaut für ein realistisches MVP.</h2>
<p> <p>
Hoard kombiniert einen modernen Frontend-Stack mit einem pragmatischen Backend-Setup, Hoard kombiniert einen modernen Frontend-Stack mit einem pragmatischen Backend-Setup,
@@ -244,15 +244,15 @@ const techStack = [
<style scoped> <style scoped>
.landing-page { .landing-page {
--hoard-page-width: 1200px; --ui-page-width: 1200px;
} }
/* ---------- Hero ---------- */ /* ---------- Hero ---------- */
.hero { .hero {
--hoard-gradient-angle: 130deg; --ui-gradient-angle: 130deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 60%; --ui-gradient-end-stop: 60%;
position: relative; position: relative;
display: grid; display: grid;
@@ -586,10 +586,10 @@ h1 {
/* ---------- Stack ---------- */ /* ---------- Stack ---------- */
.stack-section { .stack-section {
--hoard-gradient-angle: 110deg; --ui-gradient-angle: 110deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 50%, var(--color-surface) 50%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 50%, var(--color-surface) 50%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 70%; --ui-gradient-end-stop: 70%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
@@ -648,7 +648,7 @@ h1 {
.feature-section, .feature-section,
.workflow-section, .workflow-section,
.stack-section { .stack-section {
animation: hoard-soft-enter 320ms both; animation: ui-soft-enter 320ms both;
} }
.hero-preview { .hero-preview {
@@ -673,7 +673,7 @@ h1 {
} }
.preview-row { .preview-row {
animation: hoard-soft-enter 240ms both; animation: ui-soft-enter 240ms both;
} }
.preview-row:nth-child(2) { .preview-row:nth-child(2) {
@@ -778,7 +778,7 @@ h1 {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
} }
.preview-row .hoard-status { .preview-row .ui-status {
display: none; display: none;
} }
} }
+24 -24
View File
@@ -13,8 +13,8 @@ const registerDetails = [
const contactDetails = [ const contactDetails = [
{ label: 'Telefon', value: '+49 30 1234567-0', href: 'tel:+493012345670' }, { 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: 'E-Mail', value: 'kontakt@ui-demo.de', href: 'mailto:kontakt@ui-demo.de' },
{ label: 'Support', value: 'support@hoard-demo.de', href: 'mailto:support@hoard-demo.de' }, { label: 'Support', value: 'support@ui-demo.de', href: 'mailto:support@ui-demo.de' },
] ]
const legalNotes = [ const legalNotes = [
@@ -42,10 +42,10 @@ const legalNotes = [
</script> </script>
<template> <template>
<v-container fluid class="impressum-page hoard-page"> <v-container fluid class="impressum-page ui-page">
<section class="impressum-hero hoard-panel hoard-panel-gradient hoard-spotlight"> <section class="impressum-hero ui-panel ui-panel-gradient ui-spotlight">
<div class="impressum-hero__copy"> <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> <h1>Impressum</h1>
<p class="impressum-hero__lead"> <p class="impressum-hero__lead">
Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Vor produktivem Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Vor produktivem
@@ -53,16 +53,16 @@ const legalNotes = [
</p> </p>
<div class="impressum-hero__meta"> <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 <v-icon icon="mdi-information-outline" size="14" /> Testdaten
</span> </span>
<span class="hoard-chip"> <span class="ui-chip">
<v-icon icon="mdi-calendar-month-outline" size="14" /> Stand: 26. April 2026 <v-icon icon="mdi-calendar-month-outline" size="14" /> Stand: 26. April 2026
</span> </span>
</div> </div>
</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"> <v-btn variant="elevated" prepend-icon="mdi-home-outline" to="/welcome">
Zur Startseite Zur Startseite
</v-btn> </v-btn>
@@ -71,9 +71,9 @@ const legalNotes = [
</section> </section>
<section class="details-grid"> <section class="details-grid">
<article class="detail-card hoard-panel"> <article class="detail-card ui-panel">
<header class="detail-card__head"> <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> <h2>Anbieterangaben</h2>
</header> </header>
<dl class="detail-list"> <dl class="detail-list">
@@ -84,9 +84,9 @@ const legalNotes = [
</dl> </dl>
</article> </article>
<article class="detail-card hoard-panel"> <article class="detail-card ui-panel">
<header class="detail-card__head"> <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> <h2>Kontakt</h2>
</header> </header>
<dl class="detail-list"> <dl class="detail-list">
@@ -99,9 +99,9 @@ const legalNotes = [
</dl> </dl>
</article> </article>
<article class="detail-card hoard-panel"> <article class="detail-card ui-panel">
<header class="detail-card__head"> <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> <h2>Register &amp; Steuer</h2>
</header> </header>
<dl class="detail-list"> <dl class="detail-list">
@@ -113,16 +113,16 @@ const legalNotes = [
</article> </article>
</section> </section>
<section class="notes-section hoard-panel"> <section class="notes-section ui-panel">
<header class="hoard-section-head"> <header class="ui-section-head">
<p class="hoard-kicker">Rechtliche Hinweise</p> <p class="ui-kicker">Rechtliche Hinweise</p>
<h2>Wichtige Zusatzinformationen</h2> <h2>Wichtige Zusatzinformationen</h2>
<p>Standardklauseln, die im Produktivbetrieb durch eine juristische Prüfung ersetzt werden sollten.</p> <p>Standardklauseln, die im Produktivbetrieb durch eine juristische Prüfung ersetzt werden sollten.</p>
</header> </header>
<div class="notes-grid"> <div class="notes-grid">
<article v-for="note in legalNotes" :key="note.title" class="note-card"> <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" /> <v-icon :icon="note.icon" size="20" />
</span> </span>
<div> <div>
@@ -137,15 +137,15 @@ const legalNotes = [
<style scoped> <style scoped>
.impressum-page { .impressum-page {
--hoard-page-width: 1180px; --ui-page-width: 1180px;
} }
/* ---------- Hero ---------- */ /* ---------- Hero ---------- */
.impressum-hero { .impressum-hero {
--hoard-gradient-angle: 130deg; --ui-gradient-angle: 130deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.2fr) auto; grid-template-columns: minmax(0, 1.2fr) auto;
@@ -296,7 +296,7 @@ dd {
.impressum-hero, .impressum-hero,
.detail-card, .detail-card,
.notes-section { .notes-section {
animation: hoard-soft-enter 280ms both; animation: ui-soft-enter 280ms both;
} }
.detail-card:nth-child(2) { animation-delay: 80ms; } .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 { fetchAdminUserById, type AdminUser } from '@/services/adminUsers'
import { AuthRequestError } from '@/services/authSession' import { AuthRequestError } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners' import { useAppBannersStore } from '@/stores/appBanners'
import StatusPill from '@/components/ui/StatusPill.vue'
import UserAvatar from '@/components/ui/UserAvatar.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -95,15 +97,15 @@ onMounted(() => {
</script> </script>
<template> <template>
<v-container fluid class="admin-user-detail-page hoard-page"> <v-container fluid class="admin-user-detail-page ui-page">
<header class="admin-user-detail-head hoard-panel hoard-panel-gradient"> <header class="admin-user-detail-head ui-panel ui-panel-gradient">
<div class="admin-user-detail-head__copy"> <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> <h1>Benutzerdetails</h1>
<p>Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.</p> <p>Read-only Ansicht des ausgewählten Kontos. Änderungen erfolgen aktuell außerhalb der App.</p>
</div> </div>
<div class="admin-user-detail-head__actions hoard-action-row"> <div class="admin-user-detail-head__actions ui-action-row">
<v-btn <v-btn
variant="outlined" variant="outlined"
prepend-icon="mdi-arrow-left" prepend-icon="mdi-arrow-left"
@@ -129,35 +131,25 @@ onMounted(() => {
<p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen </p> <p v-else-if="isLoading" class="admin-user-detail-loading">Benutzerdetails werden geladen </p>
<article v-else-if="user" class="admin-user-detail-card hoard-panel"> <article v-else-if="user" class="admin-user-detail-card ui-panel">
<header class="admin-user-detail-card__head"> <header class="admin-user-detail-card__head">
<span class="admin-user-detail-avatar">{{ userInitials }}</span> <UserAvatar class="admin-user-detail-avatar" :initials="userInitials" />
<div> <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> <h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
<p class="admin-user-detail-card__id">{{ user.id }}</p> <p class="admin-user-detail-card__id">{{ user.id }}</p>
</div> </div>
<div class="admin-user-detail-card__pills"> <div class="admin-user-detail-card__pills">
<span <StatusPill :variant="user.isActive ? 'success' : 'danger'">
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }} {{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span> </StatusPill>
<span <StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Passwort wechseln' : 'Passwort aktuell' }} {{ user.mustChangePassword ? 'Passwort wechseln' : 'Passwort aktuell' }}
</span> </StatusPill>
</div> </div>
</header> </header>
<hr class="hoard-divider-soft" /> <hr class="ui-divider-soft" />
<dl class="admin-user-detail-grid"> <dl class="admin-user-detail-grid">
<div class="admin-user-detail-item"> <div class="admin-user-detail-item">
@@ -187,14 +179,14 @@ onMounted(() => {
<style scoped> <style scoped>
.admin-user-detail-page { .admin-user-detail-page {
--hoard-page-width: 1080px; --ui-page-width: 1080px;
} }
.admin-user-detail-head { .admin-user-detail-head {
--hoard-gradient-angle: 120deg; --ui-gradient-angle: 120deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.1fr) auto; grid-template-columns: minmax(0, 1.1fr) auto;
@@ -315,7 +307,7 @@ onMounted(() => {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.admin-user-detail-head, .admin-user-detail-head,
.admin-user-detail-card { .admin-user-detail-card {
animation: hoard-soft-enter 280ms both; animation: ui-soft-enter 280ms both;
} }
.admin-user-detail-card { .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 { fetchAdminUsers, type AdminUser } from '@/services/adminUsers'
import { AuthRequestError } from '@/services/authSession' import { AuthRequestError } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners' 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 router = useRouter()
const appBannersStore = useAppBannersStore() const appBannersStore = useAppBannersStore()
@@ -111,15 +114,15 @@ onMounted(() => {
</script> </script>
<template> <template>
<v-container fluid class="admin-users-page hoard-page"> <v-container fluid class="admin-users-page ui-page">
<header class="admin-users-header hoard-panel hoard-panel-gradient"> <header class="admin-users-header ui-panel ui-panel-gradient">
<div class="admin-users-header__copy"> <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> <h1>Benutzerverwaltung</h1>
<p>Alle Hoard-Konten mit Rollen, Status und Passwortwechselpflicht read-only.</p> <p>Alle Hoard-Konten mit Rollen, Status und Passwortwechselpflicht read-only.</p>
</div> </div>
<div class="admin-users-header__actions hoard-action-row"> <div class="admin-users-header__actions ui-action-row">
<v-text-field <v-text-field
v-model="searchQuery" v-model="searchQuery"
variant="outlined" variant="outlined"
@@ -144,7 +147,7 @@ onMounted(() => {
<section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht"> <section v-if="!isLoading && !errorMessage" class="admin-users-stats" aria-label="Benutzerübersicht">
<article class="admin-users-stat"> <article class="admin-users-stat">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-account-group-outline" size="20" /> <v-icon icon="mdi-account-group-outline" size="20" />
</span> </span>
<div> <div>
@@ -153,7 +156,7 @@ onMounted(() => {
</div> </div>
</article> </article>
<article class="admin-users-stat"> <article class="admin-users-stat">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-account-check-outline" size="20" /> <v-icon icon="mdi-account-check-outline" size="20" />
</span> </span>
<div> <div>
@@ -162,7 +165,7 @@ onMounted(() => {
</div> </div>
</article> </article>
<article class="admin-users-stat"> <article class="admin-users-stat">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-shield-account-outline" size="20" /> <v-icon icon="mdi-shield-account-outline" size="20" />
</span> </span>
<div> <div>
@@ -171,7 +174,7 @@ onMounted(() => {
</div> </div>
</article> </article>
<article class="admin-users-stat"> <article class="admin-users-stat">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-lock-reset" size="20" /> <v-icon icon="mdi-lock-reset" size="20" />
</span> </span>
<div> <div>
@@ -187,16 +190,16 @@ onMounted(() => {
<p v-else-if="isLoading" class="admin-users-loading">Benutzer werden geladen </p> <p v-else-if="isLoading" class="admin-users-loading">Benutzer werden geladen </p>
<section v-else-if="!hasUsers" class="hoard-panel hoard-empty-state"> <EmptyState
<span class="hoard-icon-tile hoard-icon-tile--lg"> v-else-if="!hasUsers"
<v-icon icon="mdi-account-question-outline" size="22" /> icon="mdi-account-question-outline"
</span> title="Keine Benutzer gefunden"
<h2>Keine Benutzer gefunden</h2> >
<p>Aktuell sind keine Konten vorhanden. Ein neuer Account muss vom Admin manuell angelegt werden.</p> Aktuell sind keine Konten vorhanden. Ein neuer Account muss vom Admin manuell angelegt werden.
</section> </EmptyState>
<section v-else class="admin-users-listing hoard-panel"> <section v-else class="admin-users-listing ui-panel">
<header class="admin-users-listing__head hoard-toolbar"> <header class="admin-users-listing__head ui-toolbar">
<div> <div>
<p class="admin-users-listing__title">Benutzer</p> <p class="admin-users-listing__title">Benutzer</p>
<p class="admin-users-listing__meta">{{ filteredUsers.length }} von {{ users.length }} angezeigt</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"> <tr v-for="user in filteredUsers" :key="user.id">
<td> <td>
<div class="admin-users-cell-user"> <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> <div>
<p class="admin-users-cell-user__name">{{ user.userName || '(ohne Benutzername)' }}</p> <p class="admin-users-cell-user__name">{{ user.userName || '(ohne Benutzername)' }}</p>
<p class="admin-users-cell-user__id">{{ user.id }}</p> <p class="admin-users-cell-user__id">{{ user.id }}</p>
@@ -231,24 +234,14 @@ onMounted(() => {
</td> </td>
<td>{{ formatRoles(user.roles) }}</td> <td>{{ formatRoles(user.roles) }}</td>
<td> <td>
<span <StatusPill :variant="user.isActive ? 'success' : 'danger'">
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }} {{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span> </StatusPill>
</td> </td>
<td> <td>
<span <StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Erforderlich' : 'Aktuell' }} {{ user.mustChangePassword ? 'Erforderlich' : 'Aktuell' }}
</span> </StatusPill>
</td> </td>
<td class="admin-users-col-actions"> <td class="admin-users-col-actions">
<v-btn <v-btn
@@ -268,7 +261,7 @@ onMounted(() => {
<div v-if="filteredUsers.length > 0" class="admin-users-mobile-list" aria-label="Benutzerliste"> <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"> <article v-for="user in filteredUsers" :key="user.id" class="admin-users-mobile-card">
<header class="admin-users-mobile-head"> <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> <div>
<p class="admin-users-mobile-label">Benutzer</p> <p class="admin-users-mobile-label">Benutzer</p>
<h2>{{ user.userName || '(ohne Benutzername)' }}</h2> <h2>{{ user.userName || '(ohne Benutzername)' }}</h2>
@@ -284,27 +277,17 @@ onMounted(() => {
<div> <div>
<dt>Aktiv</dt> <dt>Aktiv</dt>
<dd> <dd>
<span <StatusPill :variant="user.isActive ? 'success' : 'danger'">
:class="[
'hoard-status',
user.isActive ? 'hoard-status--success' : 'hoard-status--danger',
]"
>
{{ user.isActive ? 'Aktiv' : 'Inaktiv' }} {{ user.isActive ? 'Aktiv' : 'Inaktiv' }}
</span> </StatusPill>
</dd> </dd>
</div> </div>
<div> <div>
<dt>Passwortwechsel</dt> <dt>Passwortwechsel</dt>
<dd> <dd>
<span <StatusPill :variant="user.mustChangePassword ? 'warning' : 'info'">
:class="[
'hoard-status',
user.mustChangePassword ? 'hoard-status--warning' : 'hoard-status--info',
]"
>
{{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }} {{ user.mustChangePassword ? 'Erforderlich' : 'Nein' }}
</span> </StatusPill>
</dd> </dd>
</div> </div>
</dl> </dl>
@@ -325,15 +308,15 @@ onMounted(() => {
<style scoped> <style scoped>
.admin-users-page { .admin-users-page {
--hoard-page-width: 1200px; --ui-page-width: 1200px;
} }
/* ---------- Header ---------- */ /* ---------- Header ---------- */
.admin-users-header { .admin-users-header {
--hoard-gradient-angle: 120deg; --ui-gradient-angle: 120deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 55%, var(--color-surface) 45%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
@@ -505,7 +488,7 @@ onMounted(() => {
white-space: nowrap; white-space: nowrap;
} }
.hoard-empty-state { .ui-empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -516,7 +499,7 @@ onMounted(() => {
.admin-users-header, .admin-users-header,
.admin-users-stat, .admin-users-stat,
.admin-users-listing { .admin-users-listing {
animation: hoard-soft-enter 280ms both; animation: ui-soft-enter 280ms both;
} }
.admin-users-stat:nth-child(2) { animation-delay: 60ms; } .admin-users-stat:nth-child(2) { animation-delay: 60ms; }
@@ -98,14 +98,14 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<v-container fluid class="change-password-page hoard-page hoard-page--centered"> <v-container fluid class="change-password-page ui-page ui-page--centered">
<section class="change-password-shell hoard-panel hoard-shell-grid"> <section class="change-password-shell ui-panel ui-shell-grid">
<header class="change-password-head"> <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" /> <v-icon icon="mdi-shield-key-outline" size="24" />
</span> </span>
<div> <div>
<p class="hoard-kicker hoard-kicker--xs">Sicherheitsvorgabe</p> <p class="ui-kicker ui-kicker--xs">Sicherheitsvorgabe</p>
<h1>Passwort ändern</h1> <h1>Passwort ändern</h1>
<p>Aktualisiere dein Hoard-Passwort. Nach der Änderung wirst du erneut zur Anmeldung weitergeleitet.</p> <p>Aktualisiere dein Hoard-Passwort. Nach der Änderung wirst du erneut zur Anmeldung weitergeleitet.</p>
</div> </div>
@@ -121,7 +121,7 @@ async function handleSubmit() {
<v-form class="change-password-form" @submit.prevent="handleSubmit"> <v-form class="change-password-form" @submit.prevent="handleSubmit">
<section class="change-password-section"> <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-text-field
v-model="oldPassword" v-model="oldPassword"
label="Altes Passwort" label="Altes Passwort"
@@ -135,10 +135,10 @@ async function handleSubmit() {
/> />
</section> </section>
<hr class="hoard-divider-soft" /> <hr class="ui-divider-soft" />
<section class="change-password-section"> <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-text-field
v-model="newPassword" v-model="newPassword"
@@ -204,7 +204,7 @@ async function handleSubmit() {
<style scoped> <style scoped>
.change-password-page { .change-password-page {
--hoard-shell-width: min(720px, 100%); --ui-shell-width: min(720px, 100%);
} }
.change-password-shell { .change-password-shell {
@@ -286,7 +286,7 @@ async function handleSubmit() {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.change-password-shell { .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> </script>
<template> <template>
<v-container fluid class="login-page hoard-page hoard-page--centered"> <v-container fluid class="login-page ui-page ui-page--centered">
<section class="login-shell hoard-panel hoard-panel-gradient hoard-spotlight"> <section class="login-shell ui-panel ui-panel-gradient ui-spotlight">
<aside class="login-brand"> <aside class="login-brand">
<div class="login-brand__logo"> <div class="login-brand__logo">
<span class="login-brand__halo" aria-hidden="true" /> <span class="login-brand__halo" aria-hidden="true" />
<img :src="iconImage" alt="Hoard Icon" /> <img :src="iconImage" alt="Hoard Icon" />
</div> </div>
<p class="hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p> <p class="ui-kicker ui-kicker--wide">Willkommen bei Hoard</p>
<h1> <h1>
Deine Dateien.<br /> Deine Dateien.<br />
<span class="login-brand__accent">Aufgeräumt.</span> <span class="login-brand__accent">Aufgeräumt.</span>
@@ -100,21 +100,21 @@ onMounted(() => {
<ul class="login-points"> <ul class="login-points">
<li> <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> <div>
<p class="login-points__title">Ordner und Dateien</p> <p class="login-points__title">Ordner und Dateien</p>
<p class="login-points__text">Zentral organisieren, schnell finden, sauber strukturieren.</p> <p class="login-points__text">Zentral organisieren, schnell finden, sauber strukturieren.</p>
</div> </div>
</li> </li>
<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> <div>
<p class="login-points__title">Markdown direkt im Browser</p> <p class="login-points__title">Markdown direkt im Browser</p>
<p class="login-points__text">Notizen lesen und bearbeiten, ohne externes Tool.</p> <p class="login-points__text">Notizen lesen und bearbeiten, ohne externes Tool.</p>
</div> </div>
</li> </li>
<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> <div>
<p class="login-points__title">Self-hosted &amp; sicher</p> <p class="login-points__title">Self-hosted &amp; sicher</p>
<p class="login-points__text">Cookie-Auth, Rollenmodell, deine Infrastruktur.</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"> <v-form class="login-form" @submit.prevent="handleSubmit">
<header class="login-form__head"> <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> <h2>Anmelden</h2>
<p>Melde dich mit deinem bestehenden Hoard-Konto an.</p> <p>Melde dich mit deinem bestehenden Hoard-Konto an.</p>
</header> </header>
@@ -181,19 +181,21 @@ onMounted(() => {
<style scoped> <style scoped>
.login-page { .login-page {
--hoard-centered-offset: 200px; --ui-centered-offset: 200px;
} }
.login-shell { .login-shell {
--hoard-shell-width: 1080px; --ui-shell-width: 1080px;
--hoard-shell-padding: 0; --ui-shell-padding: 0;
--hoard-shell-gap: 0; --ui-shell-gap: 0;
--hoard-gradient-angle: 120deg; --ui-gradient-angle: 120deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 60%; --ui-gradient-end-stop: 60%;
display: grid;
grid-template-columns: minmax(280px, 1.1fr) minmax(320px, 460px); grid-template-columns: minmax(280px, 1.1fr) minmax(320px, 460px);
align-items: stretch;
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
overflow: hidden; overflow: hidden;
} }
@@ -356,7 +358,7 @@ h1 {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.login-brand, .login-brand,
.login-form { .login-form {
animation: hoard-soft-enter 320ms both; animation: ui-soft-enter 320ms both;
} }
.login-form { .login-form {
@@ -366,6 +368,7 @@ h1 {
@media (width <= 960px) { @media (width <= 960px) {
.login-shell { .login-shell {
display: block;
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+25 -27
View File
@@ -3,6 +3,8 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { AuthRequestError, fetchCurrentUser, type CurrentUser } from '@/services/authSession' 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 router = useRouter()
const isLoading = ref(true) const isLoading = ref(true)
@@ -94,10 +96,10 @@ onMounted(() => {
</script> </script>
<template> <template>
<v-container fluid class="dashboard-page hoard-page"> <v-container fluid class="dashboard-page ui-page">
<header class="dashboard-greeting hoard-panel hoard-panel-gradient hoard-spotlight"> <header class="dashboard-greeting ui-panel ui-panel-gradient ui-spotlight">
<div class="dashboard-greeting__copy"> <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> <h1>
<template v-if="user">Hallo, {{ user.userName || 'willkommen' }}.</template> <template v-if="user">Hallo, {{ user.userName || 'willkommen' }}.</template>
<template v-else>Dashboard</template> <template v-else>Dashboard</template>
@@ -108,18 +110,18 @@ onMounted(() => {
</p> </p>
<div class="dashboard-greeting__chips"> <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" /> <v-icon icon="mdi-shield-account-outline" size="14" />
{{ role }} {{ role }}
</span> </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 <v-icon icon="mdi-account-outline" size="14" /> Keine Rollen
</span> </span>
</div> </div>
</div> </div>
<div v-if="user" class="dashboard-greeting__avatar"> <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> <p class="dashboard-avatar__caption">{{ user.userName }}</p>
</div> </div>
</header> </header>
@@ -133,9 +135,9 @@ onMounted(() => {
</v-alert> </v-alert>
<section v-else class="dashboard-grid"> <section v-else class="dashboard-grid">
<article class="dashboard-stat hoard-panel"> <article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head"> <div class="dashboard-stat__head">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-account-outline" size="20" /> <v-icon icon="mdi-account-outline" size="20" />
</span> </span>
<p class="dashboard-stat__label">Konto</p> <p class="dashboard-stat__label">Konto</p>
@@ -144,9 +146,9 @@ onMounted(() => {
<p class="dashboard-stat__hint">Angemeldet als interner Benutzer</p> <p class="dashboard-stat__hint">Angemeldet als interner Benutzer</p>
</article> </article>
<article class="dashboard-stat hoard-panel"> <article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head"> <div class="dashboard-stat__head">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-shield-account-outline" size="20" /> <v-icon icon="mdi-shield-account-outline" size="20" />
</span> </span>
<p class="dashboard-stat__label">Rollen</p> <p class="dashboard-stat__label">Rollen</p>
@@ -155,34 +157,30 @@ onMounted(() => {
<p class="dashboard-stat__hint">Definiert deine sichtbaren Bereiche</p> <p class="dashboard-stat__hint">Definiert deine sichtbaren Bereiche</p>
</article> </article>
<article class="dashboard-stat hoard-panel"> <article class="dashboard-stat ui-panel">
<div class="dashboard-stat__head"> <div class="dashboard-stat__head">
<span class="hoard-icon-tile"> <span class="ui-icon-tile">
<v-icon icon="mdi-pulse" size="20" /> <v-icon icon="mdi-pulse" size="20" />
</span> </span>
<p class="dashboard-stat__label">Status</p> <p class="dashboard-stat__label">Status</p>
</div> </div>
<div class="dashboard-stat__pill-row"> <div class="dashboard-stat__pill-row">
<span :class="['hoard-status', `hoard-status--${accountStatusVariant}`]"> <StatusPill :variant="accountStatusVariant">{{ accountStatusLabel }}</StatusPill>
{{ accountStatusLabel }} <StatusPill :variant="passwordStatusVariant">{{ passwordStatusLabel }}</StatusPill>
</span>
<span :class="['hoard-status', `hoard-status--${passwordStatusVariant}`]">
{{ passwordStatusLabel }}
</span>
</div> </div>
<p class="dashboard-stat__hint">Konto- und Passwortzustand</p> <p class="dashboard-stat__hint">Konto- und Passwortzustand</p>
</article> </article>
</section> </section>
<section v-if="!errorMessage" class="dashboard-detail hoard-panel"> <section v-if="!errorMessage" class="dashboard-detail ui-panel">
<header class="dashboard-detail__head"> <header class="dashboard-detail__head">
<div> <div>
<p class="hoard-kicker">Auth-Antwort</p> <p class="ui-kicker">Auth-Antwort</p>
<h2>Aktueller Session-Snapshot</h2> <h2>Aktueller Session-Snapshot</h2>
<p>Direkt aus <code>GET /auth/me</code> nützlich zum Debuggen und für Plausibilitätschecks.</p> <p>Direkt aus <code>GET /auth/me</code> nützlich zum Debuggen und für Plausibilitätschecks.</p>
</div> </div>
<div class="dashboard-detail__actions hoard-action-row"> <div class="dashboard-detail__actions ui-action-row">
<v-btn <v-btn
variant="outlined" variant="outlined"
prepend-icon="mdi-lock-reset" prepend-icon="mdi-lock-reset"
@@ -210,15 +208,15 @@ onMounted(() => {
<style scoped> <style scoped>
.dashboard-page { .dashboard-page {
--hoard-page-width: 1120px; --ui-page-width: 1120px;
} }
/* ---------- Greeting ---------- */ /* ---------- Greeting ---------- */
.dashboard-greeting { .dashboard-greeting {
--hoard-gradient-angle: 120deg; --ui-gradient-angle: 120deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%); --ui-gradient-start: color-mix(in srgb, var(--color-primary-100) 60%, var(--color-surface) 40%);
--hoard-gradient-end: var(--color-surface); --ui-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 65%; --ui-gradient-end-stop: 65%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -394,7 +392,7 @@ h1 {
.dashboard-greeting, .dashboard-greeting,
.dashboard-stat, .dashboard-stat,
.dashboard-detail { .dashboard-detail {
animation: hoard-soft-enter 280ms both; animation: ui-soft-enter 280ms both;
} }
.dashboard-stat:nth-child(2) { .dashboard-stat:nth-child(2) {
+42 -42
View File
@@ -2,54 +2,54 @@
Hoard Page-Layout-Primitives Hoard Page-Layout-Primitives
============================================================================= */ ============================================================================= */
.hoard-page { .ui-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--hoard-page-gap, var(--space-6)); gap: var(--ui-page-gap, var(--space-6));
margin-inline: auto; margin-inline: auto;
width: min(100%, var(--hoard-page-width, 1180px)); width: min(100%, var(--ui-page-width, 1180px));
padding-block: padding-block:
var(--hoard-page-padding-start, var(--space-5)) var(--ui-page-padding-start, var(--space-5))
var(--hoard-page-padding-end, var(--space-12)); var(--ui-page-padding-end, var(--space-12));
} }
.hoard-page--centered { .ui-page--centered {
width: 100%; width: 100%;
margin-inline: 0; margin-inline: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: calc(100vh - var(--hoard-centered-offset, 220px)); min-height: calc(100vh - var(--ui-centered-offset, 220px));
padding: var(--hoard-centered-padding, var(--space-10) var(--space-4)); padding: var(--ui-centered-padding, var(--space-10) var(--space-4));
} }
.hoard-shell-grid { .ui-shell-grid {
display: grid; display: grid;
gap: var(--hoard-shell-gap, var(--space-8)); gap: var(--ui-shell-gap, var(--space-8));
width: min(100%, var(--hoard-shell-width, 1080px)); width: min(100%, var(--ui-shell-width, 1080px));
padding: var(--hoard-shell-padding, var(--space-8)); padding: var(--ui-shell-padding, var(--space-8));
} }
.hoard-page-header { .ui-page-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-2); gap: var(--space-2);
max-width: 78ch; max-width: 78ch;
} }
.hoard-page-header > h1 { .ui-page-header > h1 {
margin: 0; margin: 0;
font-size: var(--font-size-3xl); font-size: var(--font-size-3xl);
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.hoard-page-header > p { .ui-page-header > p {
margin: 0; margin: 0;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
line-height: var(--line-height-loose); line-height: var(--line-height-loose);
} }
.hoard-page-header__meta { .ui-page-header__meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-2); gap: var(--space-2);
@@ -57,57 +57,57 @@
} }
@media (width <= 960px) { @media (width <= 960px) {
.hoard-page { .ui-page {
width: 100%; width: 100%;
gap: var(--hoard-page-gap-mobile, var(--space-5)); gap: var(--ui-page-gap-mobile, var(--space-5));
padding-inline: padding-inline:
var(--hoard-page-padding-inline-start-mobile, max(var(--space-3), env(safe-area-inset-left))) var(--ui-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-end-mobile, max(var(--space-3), env(safe-area-inset-right)));
padding-block: padding-block:
var(--hoard-page-padding-start-mobile, var(--space-3)) var(--ui-page-padding-start-mobile, var(--space-3))
var(--hoard-page-padding-end-mobile, var(--space-8)); var(--ui-page-padding-end-mobile, var(--space-8));
} }
.hoard-page--centered { .ui-page--centered {
width: 100%; width: 100%;
min-height: calc(100vh - var(--hoard-centered-offset-mobile, 200px)); min-height: calc(100vh - var(--ui-centered-offset-mobile, 200px));
padding: var(--hoard-centered-padding-mobile, var(--space-6) var(--space-3)); padding: var(--ui-centered-padding-mobile, var(--space-6) var(--space-3));
} }
.hoard-shell-grid { .ui-shell-grid {
width: 100%; width: 100%;
gap: var(--hoard-shell-gap-mobile, var(--space-5)); gap: var(--ui-shell-gap-mobile, var(--space-5));
padding: padding:
var(--hoard-shell-padding-block-mobile, var(--space-6)) var(--ui-shell-padding-block-mobile, var(--space-6))
var(--hoard-shell-padding-inline-mobile, var(--space-5)); var(--ui-shell-padding-inline-mobile, var(--space-5));
} }
.hoard-page-header > h1 { .ui-page-header > h1 {
font-size: var(--font-size-2xl); font-size: var(--font-size-2xl);
} }
.hoard-page-header > p { .ui-page-header > p {
font-size: var(--font-size-md); font-size: var(--font-size-md);
} }
} }
@media (width <= 600px) { @media (width <= 600px) {
.hoard-page { .ui-page {
gap: var(--hoard-page-gap-mobile-xs, var(--space-4)); gap: var(--ui-page-gap-mobile-xs, var(--space-4));
padding-block: padding-block:
var(--hoard-page-padding-start-mobile-xs, var(--space-2)) var(--ui-page-padding-start-mobile-xs, var(--space-2))
var(--hoard-page-padding-end-mobile-xs, var(--space-6)); var(--ui-page-padding-end-mobile-xs, var(--space-6));
} }
.hoard-page--centered { .ui-page--centered {
min-height: calc(100vh - var(--hoard-centered-offset-mobile-xs, 180px)); min-height: calc(100vh - var(--ui-centered-offset-mobile-xs, 180px));
padding: var(--hoard-centered-padding-mobile-xs, var(--space-5) var(--space-3)); padding: var(--ui-centered-padding-mobile-xs, var(--space-5) var(--space-3));
} }
.hoard-shell-grid { .ui-shell-grid {
gap: var(--hoard-shell-gap-mobile-xs, var(--space-4)); gap: var(--ui-shell-gap-mobile-xs, var(--space-4));
padding: padding:
var(--hoard-shell-padding-block-mobile-xs, var(--space-5)) var(--ui-shell-padding-block-mobile-xs, var(--space-5))
var(--hoard-shell-padding-inline-mobile-xs, var(--space-4)); var(--ui-shell-padding-inline-mobile-xs, var(--space-4));
} }
} }
+36 -36
View File
@@ -2,7 +2,7 @@
Hoard Surface- und Inhaltspattern Hoard Surface- und Inhaltspattern
============================================================================= */ ============================================================================= */
.hoard-kicker { .ui-kicker {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
@@ -14,7 +14,7 @@
text-transform: uppercase; text-transform: uppercase;
} }
.hoard-kicker::before { .ui-kicker::before {
content: ''; content: '';
width: 18px; width: 18px;
height: 1px; height: 1px;
@@ -22,32 +22,32 @@
opacity: 0.6; opacity: 0.6;
} }
.hoard-kicker--wide { .ui-kicker--wide {
letter-spacing: 0.12em; letter-spacing: 0.12em;
} }
.hoard-kicker--xs { .ui-kicker--xs {
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
letter-spacing: 0.1em; letter-spacing: 0.1em;
} }
.hoard-kicker--plain::before { .ui-kicker--plain::before {
display: none; display: none;
} }
.hoard-action-row { .ui-action-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
} }
.hoard-action-row--end { .ui-action-row--end {
justify-content: flex-end; justify-content: flex-end;
} }
.hoard-panel-gradient { .ui-panel-gradient {
background: background:
radial-gradient( radial-gradient(
120% 80% at 100% 0%, 120% 80% at 100% 0%,
@@ -55,21 +55,21 @@
transparent 60% transparent 60%
), ),
linear-gradient( linear-gradient(
var(--hoard-gradient-angle, 130deg), var(--ui-gradient-angle, 130deg),
var(--hoard-gradient-start, color-mix(in srgb, var(--color-primary-100) 65%, var(--color-surface) 35%)) var(--ui-gradient-start, color-mix(in srgb, var(--color-primary-100) 65%, var(--color-surface) 35%))
0%, 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; position: relative;
overflow: hidden; overflow: hidden;
isolation: isolate; isolation: isolate;
} }
.hoard-spotlight::before, .ui-spotlight::before,
.hoard-spotlight::after { .ui-spotlight::after {
content: ''; content: '';
position: absolute; position: absolute;
z-index: -1; z-index: -1;
@@ -78,7 +78,7 @@
filter: blur(60px); filter: blur(60px);
} }
.hoard-spotlight::before { .ui-spotlight::before {
inset: -10% auto auto -10%; inset: -10% auto auto -10%;
width: 360px; width: 360px;
height: 360px; height: 360px;
@@ -86,7 +86,7 @@
opacity: 0.55; opacity: 0.55;
} }
.hoard-spotlight::after { .ui-spotlight::after {
inset: auto -10% -10% auto; inset: auto -10% -10% auto;
width: 320px; width: 320px;
height: 320px; height: 320px;
@@ -94,17 +94,17 @@
opacity: 0.35; 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); background: color-mix(in srgb, var(--color-primary-700) 50%, transparent);
opacity: 0.45; 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); background: color-mix(in srgb, var(--color-primary-600) 30%, transparent);
opacity: 0.4; opacity: 0.4;
} }
.hoard-chip { .ui-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
@@ -119,23 +119,23 @@
white-space: nowrap; white-space: nowrap;
} }
.hoard-chip--brand { .ui-chip--brand {
border-color: color-mix(in srgb, var(--color-primary-300) 60%, var(--color-border) 40%); 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%); background-color: color-mix(in srgb, var(--color-primary-100) 70%, var(--color-surface) 30%);
color: var(--color-primary-700); color: var(--color-primary-700);
} }
.hoard-chip--ghost { .ui-chip--ghost {
border-color: var(--color-border-subtle); border-color: var(--color-border-subtle);
background-color: transparent; background-color: transparent;
} }
.hoard-chip > .v-icon { .ui-chip > .v-icon {
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
.hoard-icon-tile { .ui-icon-tile {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -153,19 +153,19 @@
flex: 0 0 auto; flex: 0 0 auto;
} }
.hoard-icon-tile--lg { .ui-icon-tile--lg {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
} }
.hoard-icon-tile--ghost { .ui-icon-tile--ghost {
color: var(--color-text-secondary); color: var(--color-text-secondary);
background: var(--color-surface); background: var(--color-surface);
border-color: var(--color-border); border-color: var(--color-border);
} }
.hoard-divider-soft { .ui-divider-soft {
border: 0; border: 0;
height: 1px; height: 1px;
background: background:
@@ -179,7 +179,7 @@
); );
} }
.hoard-section-head { .ui-section-head {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-2); gap: var(--space-2);
@@ -187,24 +187,24 @@
max-width: 70ch; max-width: 70ch;
} }
.hoard-section-head > h2 { .ui-section-head > h2 {
margin: 0; margin: 0;
font-size: var(--font-size-2xl); font-size: var(--font-size-2xl);
letter-spacing: -0.015em; letter-spacing: -0.015em;
} }
.hoard-section-head > p { .ui-section-head > p {
margin: 0; margin: 0;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
@media (width <= 960px) { @media (width <= 960px) {
.hoard-action-row { .ui-action-row {
gap: var(--space-2); gap: var(--space-2);
} }
.hoard-spotlight::before, .ui-spotlight::before,
.hoard-spotlight::after { .ui-spotlight::after {
width: 240px; width: 240px;
height: 240px; height: 240px;
filter: blur(48px); filter: blur(48px);
@@ -212,21 +212,21 @@
} }
@media (width <= 600px) { @media (width <= 600px) {
.hoard-kicker { .ui-kicker {
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
} }
.hoard-action-row { .ui-action-row {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
width: 100%; width: 100%;
} }
.hoard-action-row > * { .ui-action-row > * {
width: 100%; width: 100%;
} }
.hoard-section-head > h2 { .ui-section-head > h2 {
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
} }
} }
+6 -6
View File
@@ -126,7 +126,7 @@ Modulare Skala in 4er-Schritten:
--space-16: 64px --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) ## Formensprache (Border-Radien)
- `--radius-xs`: 6px kleine Pills, Status-Badges. - `--radius-xs`: 6px kleine Pills, Status-Badges.
@@ -214,9 +214,9 @@ Regeln:
- Hover: sehr subtile Primary-Tint-Tönung. - Hover: sehr subtile Primary-Tint-Tönung.
- Selected Row: Primary-100 Hintergrund, `--color-primary-700` Text. - Selected Row: Primary-100 Hintergrund, `--color-primary-700` Text.
- Border-Bottom in `--color-border-subtle`. - 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). - Kompakt, `--radius-xs`, mit dezentem Text/Background-Tint pro Status (Success/Info/Warning/Danger/Neutral).
- Optional kleines Punktindikator-Dot vor dem Text. - Optional kleines Punktindikator-Dot vor dem Text.
@@ -227,11 +227,11 @@ Regeln:
- Enter/Leave: 180 ms Fade + 10 px Y. - Enter/Leave: 180 ms Fade + 10 px Y.
## Hero-/Marketing-Bereiche ## 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). - Optional dezenter „Spotlight"-Glow (radialer Verlauf, `--color-primary-300` mit niedriger Opazität, blur).
- Inhaltsbreite max ~64ch. - Inhaltsbreite max ~64ch.
- Display-Headlines mit `--font-size-3xl` oder `--font-size-display` (nur Hero). - 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) ## Vorschau-Bereich (Files)
- **PDF-Vorschau:** neutraler Hintergrund, weiße „Papier"-Fläche mit `--shadow-md`, genug Rand. Controls minimal. - **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 ### Immer
- Tokens nutzen (Farben, Spacing, Radius, Shadow). - 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. - Light- und Dark-Mode gleichwertig prüfen.
- Animationen kurz halten und `prefers-reduced-motion` respektieren. - Animationen kurz halten und `prefers-reduced-motion` respektieren.