diff --git a/backend/static/css/components.css b/backend/static/css/components.css new file mode 100644 index 0000000..0ed44de --- /dev/null +++ b/backend/static/css/components.css @@ -0,0 +1,681 @@ +/* ============================================================ + BAN YARO — Komponenten + Alle wiederverwendbaren UI-Bausteine an einem Ort. + Kein Baustein wird zweimal definiert. + ============================================================ */ + +/* ------------------------------------------------------------ + 1. BUTTONS + ------------------------------------------------------------ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-5); + font-size: var(--text-base); + font-weight: var(--weight-semibold); + line-height: 1; + border-radius: var(--radius-md); + border: 2px solid transparent; + transition: background var(--transition-fast), + color var(--transition-fast), + border-color var(--transition-fast), + transform var(--transition-fast), + box-shadow var(--transition-fast); + cursor: pointer; + white-space: nowrap; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + min-height: 44px; /* Touch-Target Minimum */ +} + +.btn:active { transform: scale(0.97); } + +.btn-primary { + background: var(--c-primary); + color: var(--c-text-inverse); + border-color:var(--c-primary); +} +.btn-primary:hover { + background: var(--c-primary-dark); + border-color: var(--c-primary-dark); +} + +.btn-secondary { + background: var(--c-surface); + color: var(--c-text); + border-color: var(--c-border); + box-shadow: var(--shadow-xs); +} +.btn-secondary:hover { + background: var(--c-surface-2); + border-color: var(--c-surface-3); +} + +.btn-ghost { + background: transparent; + color: var(--c-text-secondary); + border-color: transparent; +} +.btn-ghost:hover { + background: var(--c-surface-2); + color: var(--c-text); +} + +.btn-danger { + background: var(--c-danger); + color: #fff; + border-color: var(--c-danger); +} +.btn-danger:hover { + background: var(--c-danger-dark); + border-color: var(--c-danger-dark); +} + +.btn-nature { + background: var(--c-nature); + color: #fff; + border-color: var(--c-nature); +} + +/* Größen */ +.btn-sm { + padding: var(--space-2) var(--space-3); + font-size: var(--text-sm); + min-height: 36px; + border-radius: var(--radius-sm); +} +.btn-lg { + padding: var(--space-4) var(--space-8); + font-size: var(--text-md); + min-height: 52px; + border-radius: var(--radius-lg); +} +.btn-full { width: 100%; } + +.btn:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none; +} + +/* Icon-only Button */ +.btn-icon { + padding: var(--space-2); + border-radius: var(--radius-md); + min-height: 44px; + min-width: 44px; +} + +/* ------------------------------------------------------------ + 2. CARDS + ------------------------------------------------------------ */ +.card { + background: var(--c-surface); + border: 1px solid var(--c-border-light); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.card-body { + padding: var(--space-4); +} + +.card-header { + padding: var(--space-4); + border-bottom: 1px solid var(--c-border-light); + font-weight: var(--weight-semibold); + font-size: var(--text-md); +} + +.card-footer { + padding: var(--space-3) var(--space-4); + border-top: 1px solid var(--c-border-light); + background: var(--c-bg); +} + +/* Klickbare Card */ +.card-clickable { + cursor: pointer; + transition: box-shadow var(--transition-fast), + transform var(--transition-fast); +} +.card-clickable:hover { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} +.card-clickable:active { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +/* Horizontale Card (Bild links + Inhalt rechts) */ +.card-horizontal { + display: flex; + align-items: stretch; +} +.card-horizontal .card-image { + width: 88px; + flex-shrink: 0; + object-fit: cover; +} + +/* ------------------------------------------------------------ + 3. BADGES & STATUS-PILLS + ------------------------------------------------------------ */ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1) var(--space-2); + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + border-radius: var(--radius-full); + white-space: nowrap; +} + +.badge-primary { background: var(--c-primary-subtle); color: var(--c-primary-dark); } +.badge-success { background: var(--c-success-subtle); color: var(--c-success); } +.badge-danger { background: var(--c-danger-subtle); color: var(--c-danger); } +.badge-warning { background: var(--c-warning-subtle); color: var(--c-primary-dark); } +.badge-info { background: var(--c-info-subtle); color: var(--c-info); } +.badge-nature { background: var(--c-nature-subtle); color: var(--c-nature); } +.badge-muted { background: var(--c-surface-2); color: var(--c-text-secondary); } + +/* Dot-Indicator */ +.badge-dot::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-radius: var(--radius-full); + background: currentColor; +} + +/* Giftköder-spezifisch — besonders auffällig */ +.badge-alarm { + background: var(--c-danger); + color: #fff; + font-size: var(--text-xs); + font-weight: var(--weight-bold); + animation: pulse-alarm 2s ease-in-out infinite; +} +@keyframes pulse-alarm { + 0%, 100% { box-shadow: 0 0 0 0 rgba(196, 57, 26, 0.4); } + 50% { box-shadow: 0 0 0 6px rgba(196, 57, 26, 0); } +} + +/* ------------------------------------------------------------ + 4. FORMULARE + ------------------------------------------------------------ */ +.form-group { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.form-label { + font-size: var(--text-sm); + font-weight: var(--weight-medium); + color: var(--c-text-secondary); +} + +.form-control { + width: 100%; + padding: var(--space-3) var(--space-4); + font-size: var(--text-base); + color: var(--c-text); + background: var(--c-surface); + border: 1.5px solid var(--c-border); + border-radius: var(--radius-md); + transition: border-color var(--transition-fast), + box-shadow var(--transition-fast); + min-height: 44px; + appearance: none; + -webkit-appearance: none; +} + +.form-control::placeholder { color: var(--c-text-muted); } + +.form-control:focus { + outline: none; + border-color: var(--c-primary); + box-shadow: 0 0 0 3px rgba(196, 132, 58, 0.15); +} + +.form-control:invalid:not(:placeholder-shown) { + border-color: var(--c-danger); +} + +textarea.form-control { + resize: vertical; + min-height: 100px; +} + +.form-control-icon { + position: relative; +} +.form-control-icon .form-control { + padding-left: var(--space-10); +} +.form-control-icon .icon { + position: absolute; + left: var(--space-3); + top: 50%; + transform: translateY(-50%); + color: var(--c-text-muted); + pointer-events: none; +} + +.form-hint { + font-size: var(--text-xs); + color: var(--c-text-muted); +} +.form-error { + font-size: var(--text-xs); + color: var(--c-danger); +} + +/* Toggle / Checkbox */ +.toggle { + position: relative; + display: inline-block; + width: 48px; + height: 28px; + flex-shrink: 0; +} +.toggle input { opacity: 0; width: 0; height: 0; } +.toggle-slider { + position: absolute; + inset: 0; + background: var(--c-border); + border-radius: var(--radius-full); + transition: background var(--transition-normal); + cursor: pointer; +} +.toggle-slider::before { + content: ''; + position: absolute; + width: 22px; + height: 22px; + left: 3px; + top: 3px; + background: #fff; + border-radius: var(--radius-full); + transition: transform var(--transition-normal); + box-shadow: var(--shadow-sm); +} +.toggle input:checked + .toggle-slider { + background: var(--c-primary); +} +.toggle input:checked + .toggle-slider::before { + transform: translateX(20px); +} + +/* ------------------------------------------------------------ + 5. AVATAR + ------------------------------------------------------------ */ +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-full); + overflow: hidden; + background: var(--c-surface-2); + color: var(--c-text-secondary); + font-weight: var(--weight-semibold); + flex-shrink: 0; +} +.avatar img { width: 100%; height: 100%; object-fit: cover; } + +.avatar-sm { width: 32px; height: 32px; font-size: var(--text-xs); } +.avatar-md { width: 44px; height: 44px; font-size: var(--text-sm); } +.avatar-lg { width: 64px; height: 64px; font-size: var(--text-lg); } +.avatar-xl { width: 96px; height: 96px; font-size: var(--text-2xl); } +.avatar-2xl { width: 128px; height: 128px; font-size: var(--text-3xl); } + +/* Hunde-Avatar mit Rahmen in Primärfarbe */ +.avatar-dog { + border: 3px solid var(--c-primary-light); + box-shadow: 0 0 0 1px var(--c-primary); +} + +/* ------------------------------------------------------------ + 6. LISTE (Einstellungen, Menüs) + ------------------------------------------------------------ */ +.list { + background: var(--c-surface); + border: 1px solid var(--c-border-light); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.list-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-4); + border-bottom: 1px solid var(--c-border-light); + transition: background var(--transition-fast); + min-height: 52px; +} +.list-item:last-child { border-bottom: none; } +.list-item.clickable { cursor: pointer; } +.list-item.clickable:hover { background: var(--c-bg); } + +.list-item-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 20px; +} + +.list-item-content { flex: 1; min-width: 0; } +.list-item-title { font-weight: var(--weight-medium); } +.list-item-desc { font-size: var(--text-sm); color: var(--c-text-secondary); } + +/* ------------------------------------------------------------ + 7. TOAST / BENACHRICHTIGUNGEN + ------------------------------------------------------------ */ +.toast-container { + position: fixed; + top: calc(var(--safe-top) + var(--space-4)); + left: 50%; + transform: translateX(-50%); + z-index: 1000; + display: flex; + flex-direction: column; + gap: var(--space-2); + pointer-events: none; + width: calc(100% - var(--space-8)); + max-width: 400px; +} + +.toast { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + background: var(--c-text); + color: var(--c-text-inverse); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + font-size: var(--text-sm); + font-weight: var(--weight-medium); + pointer-events: auto; + animation: toast-in var(--transition-normal) ease forwards; +} + +.toast-success { background: var(--c-success); } +.toast-danger { background: var(--c-danger); } +.toast-warning { background: var(--c-warning); color: var(--c-text); } +.toast-info { background: var(--c-info); } + +@keyframes toast-in { + from { opacity: 0; transform: translateY(-8px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes toast-out { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-8px) scale(0.96); } +} +.toast.removing { animation: toast-out var(--transition-normal) ease forwards; } + +/* ------------------------------------------------------------ + 8. MODAL + ------------------------------------------------------------ */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(42, 31, 20, 0.5); + z-index: 900; + display: flex; + align-items: flex-end; + justify-content: center; + padding: var(--space-4); + backdrop-filter: blur(2px); + animation: overlay-in var(--transition-normal) ease; +} +@media (min-width: 768px) { + .modal-overlay { align-items: center; } +} + +.modal { + background: var(--c-surface); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + width: 100%; + max-width: 480px; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-xl); + animation: modal-in var(--transition-slow) cubic-bezier(0.34, 1.56, 0.64, 1); +} +@media (min-width: 768px) { + .modal { + border-radius: var(--radius-xl); + animation: overlay-in var(--transition-normal) ease; + } +} + +.modal-handle { + width: 40px; + height: 4px; + background: var(--c-border); + border-radius: var(--radius-full); + margin: var(--space-3) auto; +} +@media (min-width: 768px) { .modal-handle { display: none; } } + +.modal-header { + padding: var(--space-4) var(--space-6); + border-bottom: 1px solid var(--c-border-light); + display: flex; + align-items: center; + justify-content:space-between; +} +.modal-title { + font-size: var(--text-lg); + font-weight: var(--weight-semibold); +} +.modal-body { padding: var(--space-6); } +.modal-footer { + padding: var(--space-4) var(--space-6); + border-top: 1px solid var(--c-border-light); + display: flex; + gap: var(--space-3); + justify-content: flex-end; +} + +@keyframes overlay-in { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes modal-in { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +/* ------------------------------------------------------------ + 9. LADEINDIKATOR + ------------------------------------------------------------ */ +.spinner { + width: 24px; + height: 24px; + border: 2.5px solid var(--c-border); + border-top-color: var(--c-primary); + border-radius: var(--radius-full); + animation: spin 0.7s linear infinite; + flex-shrink: 0; +} +.spinner-sm { width: 16px; height: 16px; border-width: 2px; } +.spinner-lg { width: 36px; height: 36px; border-width: 3px; } + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-overlay { + position: absolute; + inset: 0; + background: rgba(250, 247, 242, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + border-radius: inherit; +} + +/* ------------------------------------------------------------ + 10. LEERER ZUSTAND (Empty State) + ------------------------------------------------------------ */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: var(--space-12) var(--space-8); + gap: var(--space-4); +} +.empty-state-icon { + font-size: 48px; + line-height: 1; + margin-bottom: var(--space-2); +} +.empty-state-title { + font-size: var(--text-lg); + font-weight: var(--weight-semibold); + color: var(--c-text); +} +.empty-state-text { + font-size: var(--text-sm); + color: var(--c-text-secondary); + max-width: 280px; + line-height: var(--leading-relaxed); +} + +/* ------------------------------------------------------------ + 11. FOTO-UPLOAD + ------------------------------------------------------------ */ +.photo-upload { + position: relative; + border: 2px dashed var(--c-border); + border-radius: var(--radius-lg); + background: var(--c-bg); + display: flex; + flex-direction: column; + align-items: center; + justify-content:center; + gap: var(--space-2); + padding: var(--space-8); + cursor: pointer; + transition: border-color var(--transition-fast), + background var(--transition-fast); + min-height: 140px; +} +.photo-upload:hover { + border-color: var(--c-primary); + background: var(--c-primary-subtle); +} +.photo-upload input[type="file"] { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} +.photo-upload-icon { font-size: 32px; color: var(--c-text-muted); } +.photo-upload-text { font-size: var(--text-sm); color: var(--c-text-secondary); } +.photo-upload-hint { font-size: var(--text-xs); color: var(--c-text-muted); } + +/* Vorschau nach Upload */ +.photo-preview { + position: relative; + border-radius: var(--radius-lg); + overflow: hidden; +} +.photo-preview img { width: 100%; object-fit: cover; } +.photo-preview-remove { + position: absolute; + top: var(--space-2); + right: var(--space-2); + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: rgba(42, 31, 20, 0.6); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; +} + +/* ------------------------------------------------------------ + 12. TRENNLINIEN & ABSCHNITTE + ------------------------------------------------------------ */ +.divider { + height: 1px; + background: var(--c-border-light); + margin: var(--space-4) 0; +} + +.section-title { + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + color: var(--c-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + padding: var(--space-4) var(--space-4) var(--space-2); +} + +/* ------------------------------------------------------------ + 13. FAB — Floating Action Button (der + Button) + ------------------------------------------------------------ */ +.fab { + position: fixed; + z-index: 200; + width: 56px; + height: 56px; + border-radius: var(--radius-full); + background: var(--c-primary); + color: #fff; + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + cursor: pointer; + transition: transform var(--transition-fast), + box-shadow var(--transition-fast); + -webkit-tap-highlight-color: transparent; + + /* Mobile: über der Bottom-Nav zentriert */ + bottom: calc(var(--nav-bottom-height) + var(--safe-bottom) + var(--space-4)); + left: 50%; + transform: translateX(-50%); +} +.fab:hover { + transform: translateX(-50%) scale(1.05); + box-shadow: var(--shadow-xl); +} +.fab:active { + transform: translateX(-50%) scale(0.95); +} + +/* Desktop: FAB positioniert sich neu */ +@media (min-width: 768px) { + .fab { + bottom: var(--space-8); + left: auto; + right: var(--space-8); + transform: none; + } + .fab:hover { transform: scale(1.05); } + .fab:active { transform: scale(0.95); } +} diff --git a/backend/static/css/design-system.css b/backend/static/css/design-system.css new file mode 100644 index 0000000..1130a2a --- /dev/null +++ b/backend/static/css/design-system.css @@ -0,0 +1,262 @@ +/* ============================================================ + BAN YARO — Design System + Farben abgeleitet aus dem Foto von Ban Yaro (Parson Russell) + Mobile-first, CSS Custom Properties + ============================================================ */ + +/* ------------------------------------------------------------ + 1. TOKENS — Farben, Abstände, Typografie, Schatten + ------------------------------------------------------------ */ +:root { + /* Primärfarben — Honig-Amber aus Ban Yaros Fell */ + --c-primary: #C4843A; + --c-primary-dark: #9E6520; + --c-primary-light: #E8C48A; + --c-primary-subtle: #FAF0E0; + + /* Oberflächen — Warmweiß und Strohbeige */ + --c-bg: #FAF7F2; + --c-surface: #FFFFFF; + --c-surface-2: #EDE5D4; + --c-surface-3: #DDD0BB; + --c-border: #E0D6C8; + --c-border-light: #EDE8E0; + + /* Natur — Grün des Feldes, Blaugrau des Himmels */ + --c-nature: #6B8055; + --c-nature-light: #A0B885; + --c-nature-subtle: #EDF2E8; + --c-sky: #8FAAB8; + --c-sky-subtle: #EAF1F5; + + /* Text — Warmbraun aus dem Halsband */ + --c-text: #2A1F14; + --c-text-secondary: #7A6A58; + --c-text-muted: #B0A090; + --c-text-inverse: #FAF7F2; + + /* Funktionsfarben */ + --c-danger: #C4391A; + --c-danger-dark: #9E2A10; + --c-danger-subtle: #FDEEE9; + --c-success: #5B8A4A; + --c-success-subtle: #EBF4E7; + --c-warning: #D4923A; + --c-warning-subtle: #FDF3E3; + --c-info: #4A7A9B; + --c-info-subtle: #E8F2F8; + + /* Typografie */ + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: "SF Mono", "Fira Code", Consolas, monospace; + + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-md: 1.125rem; /* 18px */ + --text-lg: 1.25rem; /* 20px */ + --text-xl: 1.5rem; /* 24px */ + --text-2xl: 1.875rem; /* 30px */ + --text-3xl: 2.25rem; /* 36px */ + + --weight-normal: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed:1.75; + + /* Abstände (4px-Raster) */ + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + --space-12: 3rem; /* 48px */ + --space-16: 4rem; /* 64px */ + + /* Radien */ + --radius-sm: 0.375rem; /* 6px */ + --radius-md: 0.625rem; /* 10px */ + --radius-lg: 1rem; /* 16px */ + --radius-xl: 1.5rem; /* 24px */ + --radius-full: 9999px; + + /* Schatten */ + --shadow-xs: 0 1px 2px rgba(42, 31, 20, 0.06); + --shadow-sm: 0 1px 4px rgba(42, 31, 20, 0.08), 0 1px 2px rgba(42, 31, 20, 0.05); + --shadow-md: 0 4px 12px rgba(42, 31, 20, 0.10), 0 2px 4px rgba(42, 31, 20, 0.06); + --shadow-lg: 0 8px 24px rgba(42, 31, 20, 0.12), 0 4px 8px rgba(42, 31, 20, 0.06); + --shadow-xl: 0 16px 40px rgba(42, 31, 20, 0.14), 0 8px 16px rgba(42, 31, 20, 0.08); + + /* Übergänge */ + --transition-fast: 120ms ease; + --transition-normal: 200ms ease; + --transition-slow: 320ms ease; + + /* Navigation */ + --nav-bottom-height: 64px; + --nav-sidebar-width: 240px; + --header-height: 56px; + + /* Safe Areas (iPhone Notch/Home Indicator) */ + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --safe-right: env(safe-area-inset-right, 0px); +} + +/* Dark Mode — vorbereitet, nicht aktiv */ +@media (prefers-color-scheme: dark) { + :root { + --c-bg: #1A1410; + --c-surface: #241C14; + --c-surface-2: #2E2418; + --c-surface-3: #3A2E20; + --c-border: #4A3C2C; + --c-border-light: #3A2E20; + --c-text: #F0EAE0; + --c-text-secondary: #C0B0A0; + --c-text-muted: #806A58; + } +} + +/* ------------------------------------------------------------ + 2. RESET & BASE + ------------------------------------------------------------ */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; + height: 100%; +} + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + font-weight: var(--weight-normal); + line-height: var(--leading-normal); + color: var(--c-text); + background-color: var(--c-bg); + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +img, video, svg { + display: block; + max-width: 100%; +} + +a { + color: var(--c-primary); + text-decoration: none; +} + +a:hover { + color: var(--c-primary-dark); +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; +} + +input, textarea, select { + font-family: inherit; + font-size: inherit; +} + +ul, ol { + list-style: none; +} + +/* ------------------------------------------------------------ + 3. TYPOGRAFIE-HELFER + ------------------------------------------------------------ */ +.text-xs { font-size: var(--text-xs); } +.text-sm { font-size: var(--text-sm); } +.text-base { font-size: var(--text-base); } +.text-md { font-size: var(--text-md); } +.text-lg { font-size: var(--text-lg); } +.text-xl { font-size: var(--text-xl); } +.text-2xl { font-size: var(--text-2xl); } + +.font-medium { font-weight: var(--weight-medium); } +.font-semibold{ font-weight: var(--weight-semibold); } +.font-bold { font-weight: var(--weight-bold); } + +.text-primary { color: var(--c-primary); } +.text-secondary { color: var(--c-text-secondary); } +.text-muted { color: var(--c-text-muted); } +.text-danger { color: var(--c-danger); } +.text-success { color: var(--c-success); } +.text-nature { color: var(--c-nature); } + +.text-center { text-align: center; } +.text-right { text-align: right; } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ------------------------------------------------------------ + 4. LAYOUT-HELFER + ------------------------------------------------------------ */ +.flex { display: flex; } +.flex-col { display: flex; flex-direction: column; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.justify-between { justify-content: space-between; } +.justify-center { justify-content: center; } +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-6 { gap: var(--space-6); } +.flex-1 { flex: 1; } +.w-full { width: 100%; } + +/* ------------------------------------------------------------ + 5. ABSTANDS-HELFER + ------------------------------------------------------------ */ +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } +.p-6 { padding: var(--space-6); } +.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); } +.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); } +.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); } +.mt-2 { margin-top: var(--space-2); } +.mt-4 { margin-top: var(--space-4); } +.mt-6 { margin-top: var(--space-6); } +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.ml-auto { margin-left: auto; } + +/* ------------------------------------------------------------ + 6. SICHTBARKEIT + ------------------------------------------------------------ */ +.hidden { display: none !important; } + +/* Nur auf Mobile sichtbar */ +.mobile-only { display: block; } +@media (min-width: 768px) { + .mobile-only { display: none !important; } +} + +/* Nur auf Desktop sichtbar */ +.desktop-only { display: none !important; } +@media (min-width: 768px) { + .desktop-only { display: block !important; } +} diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css new file mode 100644 index 0000000..88fe171 --- /dev/null +++ b/backend/static/css/layout.css @@ -0,0 +1,373 @@ +/* ============================================================ + BAN YARO — Layout + App Shell: Mobile Bottom-Nav + Desktop Sidebar + Mobile-first mit einer einzigen Media Query für Desktop + ============================================================ */ + +/* ------------------------------------------------------------ + 1. APP SHELL + ------------------------------------------------------------ */ +#app { + display: flex; + flex-direction: column; + min-height: 100dvh; /* dvh: berücksichtigt mobile Browser-Chrome */ +} + +/* Content-Bereich: füllt den Raum zwischen Header und Bottom-Nav */ +#page-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding-bottom: calc(var(--nav-bottom-height) + var(--safe-bottom)); +} + +/* Desktop: Sidebar-Layout */ +@media (min-width: 768px) { + #app { + flex-direction: row; + } + #page-content { + padding-bottom: 0; + padding-left: var(--nav-sidebar-width); + } +} + +/* ------------------------------------------------------------ + 2. HEADER (Mobile) + ------------------------------------------------------------ */ +#app-header { + position: sticky; + top: 0; + z-index: 100; + height: calc(var(--header-height) + var(--safe-top)); + padding-top: var(--safe-top); + background: var(--c-surface); + border-bottom: 1px solid var(--c-border-light); + display: flex; + align-items: center; + padding-left: var(--space-4); + padding-right: var(--space-4); + gap: var(--space-3); + box-shadow: var(--shadow-xs); +} + +.header-title { + font-size: var(--text-lg); + font-weight: var(--weight-bold); + color: var(--c-text); + flex: 1; +} + +.header-back { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-md); + color: var(--c-text-secondary); + cursor: pointer; + transition: background var(--transition-fast); + font-size: 20px; + flex-shrink: 0; +} +.header-back:hover { background: var(--c-surface-2); } + +/* Auf Desktop keinen mobilen Header zeigen */ +@media (min-width: 768px) { + #app-header { display: none; } +} + +/* ------------------------------------------------------------ + 3. BOTTOM NAVIGATION (Mobile) + ------------------------------------------------------------ */ +#bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 300; + height: calc(var(--nav-bottom-height) + var(--safe-bottom)); + padding-bottom: var(--safe-bottom); + background: var(--c-surface); + border-top: 1px solid var(--c-border-light); + display: flex; + align-items: stretch; + box-shadow: 0 -2px 12px rgba(42, 31, 20, 0.08); +} + +.nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + cursor: pointer; + color: var(--c-text-muted); + transition: color var(--transition-fast); + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + position: relative; + padding: var(--space-2) var(--space-1); +} + +.nav-item:hover { color: var(--c-text-secondary); } +.nav-item.active { color: var(--c-primary); } + +/* Aktiv-Indikator oben */ +.nav-item.active::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 32px; + height: 3px; + background: var(--c-primary); + border-radius: 0 0 var(--radius-full) var(--radius-full); +} + +.nav-item-icon { + font-size: 22px; + line-height: 1; + position: relative; +} + +/* Ungelesen-Badge auf Nav-Icon */ +.nav-badge { + position: absolute; + top: -4px; + right: -6px; + background: var(--c-danger); + color: #fff; + font-size: 9px; + font-weight: var(--weight-bold); + min-width: 16px; + height: 16px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + padding: 0 3px; + border: 2px solid var(--c-surface); +} + +.nav-item-label { + font-size: 9px; + font-weight: var(--weight-semibold); + line-height: 1; +} + +/* Mittlerer + Button: kein Label, größer */ +.nav-item-center .nav-item-icon { + width: 50px; + height: 50px; + background: var(--c-primary); + color: #fff; + border-radius: var(--radius-full); + font-size: 26px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-md); + transition: transform var(--transition-fast), + box-shadow var(--transition-fast); + margin-top: -12px; /* ragt über die Nav hinaus */ +} +.nav-item-center:active .nav-item-icon { + transform: scale(0.93); + box-shadow: var(--shadow-sm); +} +.nav-item-center.active::before { display: none; } +.nav-item-center .nav-item-label { display: none; } + +/* Desktop: Bottom Nav ausblenden */ +@media (min-width: 768px) { + #bottom-nav { display: none; } +} + +/* ------------------------------------------------------------ + 4. SIDEBAR NAVIGATION (Desktop) + ------------------------------------------------------------ */ +#sidebar { + display: none; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: var(--nav-sidebar-width); + z-index: 300; + background: var(--c-surface); + border-right: 1px solid var(--c-border-light); + flex-direction: column; + overflow-y: auto; + box-shadow: var(--shadow-sm); +} + +@media (min-width: 768px) { + #sidebar { display: flex; } +} + +.sidebar-logo { + padding: var(--space-6) var(--space-5); + display: flex; + align-items: center; + gap: var(--space-3); + border-bottom: 1px solid var(--c-border-light); + flex-shrink: 0; +} +.sidebar-logo-img { + width: 36px; + height: 36px; + border-radius: var(--radius-full); + object-fit: cover; +} +.sidebar-logo-text { + font-size: var(--text-lg); + font-weight: var(--weight-bold); + color: var(--c-text); +} + +.sidebar-nav { + flex: 1; + padding: var(--space-4) var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.sidebar-section-label { + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + color: var(--c-text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + padding: var(--space-3) var(--space-3) var(--space-1); + margin-top: var(--space-2); +} + +.sidebar-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3); + border-radius: var(--radius-md); + cursor: pointer; + color: var(--c-text-secondary); + font-size: var(--text-sm); + font-weight: var(--weight-medium); + transition: background var(--transition-fast), + color var(--transition-fast); + -webkit-tap-highlight-color: transparent; +} +.sidebar-item:hover { + background: var(--c-bg); + color: var(--c-text); +} +.sidebar-item.active { + background: var(--c-primary-subtle); + color: var(--c-primary-dark); + font-weight: var(--weight-semibold); +} +.sidebar-item-icon { + font-size: 18px; + width: 24px; + text-align: center; + flex-shrink: 0; +} +.sidebar-item-badge { + margin-left: auto; + background: var(--c-danger); + color: #fff; + font-size: var(--text-xs); + font-weight: var(--weight-bold); + min-width: 20px; + height: 20px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + padding: 0 var(--space-1); +} + +.sidebar-footer { + padding: var(--space-4) var(--space-2); + border-top: 1px solid var(--c-border-light); + flex-shrink: 0; +} + +/* ------------------------------------------------------------ + 5. PAGE WRAPPER (inneres Layout der Seiten) + ------------------------------------------------------------ */ +.page { + display: none; + flex-direction: column; + min-height: 100%; +} +.page.active { display: flex; } + +/* Scrollbarer Seiteninhalt */ +.page-body { + flex: 1; + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +/* Volle Breite mit seitlichem Limit für Desktop */ +.page-container { + width: 100%; + max-width: 680px; + margin: 0 auto; +} + +/* Wide-Layout für Karte und ähnliches */ +.page-container-wide { + width: 100%; + max-width: 1040px; + margin: 0 auto; +} + +/* Karte nimmt vollen Platz ein */ +.page-fullscreen { + position: absolute; + inset: 0; + padding: 0; +} + +/* ------------------------------------------------------------ + 6. PULL-TO-REFRESH INDIKATOR + ------------------------------------------------------------ */ +.ptr-indicator { + display: flex; + justify-content: center; + align-items: center; + height: 48px; + color: var(--c-text-muted); + font-size: var(--text-sm); + gap: var(--space-2); + overflow: hidden; + max-height: 0; + transition: max-height var(--transition-normal); +} +.ptr-indicator.visible { max-height: 48px; } + +/* ------------------------------------------------------------ + 7. RESPONSIVE GRID + ------------------------------------------------------------ */ +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); +} +.grid-3 { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: var(--space-3); +} + +@media (max-width: 400px) { + .grid-3 { grid-template-columns: 1fr 1fr; } +} diff --git a/backend/static/index.html b/backend/static/index.html new file mode 100644 index 0000000..d5c5e22 --- /dev/null +++ b/backend/static/index.html @@ -0,0 +1,229 @@ + + +
+ + + + + + + + + + + + +${message}
`, + footer: ` + + + `, + onClose: () => resolve(false), + }); + m.parentElement.querySelector('#modal-cancel').addEventListener('click', () => { + close(); resolve(false); + }); + m.parentElement.querySelector('#modal-confirm').addEventListener('click', () => { + close(); resolve(true); + }); + }); + } + + return { open, close, confirm }; + })(); + + // ---------------------------------------------------------- + // LOADING STATE für Buttons + // ---------------------------------------------------------- + function setLoading(btn, loading) { + if (loading) { + btn._originalContent = btn.innerHTML; + btn.innerHTML = ''; + btn.disabled = true; + } else { + btn.innerHTML = btn._originalContent || btn.innerHTML; + btn.disabled = false; + } + } + + // ---------------------------------------------------------- + // ASYNC BUTTON: Button-Click → Loader → Ergebnis → Toast + // Verwendung: UI.asyncButton(btn, async () => { await API.something() }) + // ---------------------------------------------------------- + async function asyncButton(btn, fn, { successMsg, errorMsg } = {}) { + setLoading(btn, true); + try { + const result = await fn(); + if (successMsg) toast.success(successMsg); + return result; + } catch (err) { + const msg = errorMsg || err.message || 'Ein Fehler ist aufgetreten.'; + toast.error(msg); + throw err; + } finally { + setLoading(btn, false); + } + } + + // ---------------------------------------------------------- + // FORMULAR-HELPER + // ---------------------------------------------------------- + function formData(form) { + const data = {}; + new FormData(form).forEach((v, k) => { data[k] = v; }); + return data; + } + + function setFormError(form, fieldName, message) { + const field = form.querySelector(`[name="${fieldName}"]`); + if (!field) return; + field.classList.add('is-invalid'); + let hint = field.parentElement.querySelector('.form-error'); + if (!hint) { + hint = document.createElement('span'); + hint.className = 'form-error'; + field.parentElement.appendChild(hint); + } + hint.textContent = message; + } + + function clearFormErrors(form) { + form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); + form.querySelectorAll('.form-error').forEach(el => el.remove()); + } + + // ---------------------------------------------------------- + // LEERER ZUSTAND (Empty State) + // ---------------------------------------------------------- + function emptyState({ icon, title, text, action } = {}) { + return ` +