Beim Laden stand ein Kreisel in der Mitte. Der sagt „warte", aber nicht, worauf – und wenn die Daten kommen, klappt das Layout um. Jetzt steht dort ein Platzhalter in der Form des Kommenden: Kacheln in der Kachelansicht, Zeilen in der Liste. Die Maße sind aus FileViews.tsx abgeschrieben, nicht geschätzt; weichen sie ab, springt es beim Wechsel wieder und der Zweck wäre dahin. Leerzustände sagen jetzt, warum nichts da ist, und bieten den Knopf an, der an dieser Stelle weiterhilft. Der leere Papierkorb bekommt bewusst keinen: Er ist ein guter Zustand, kein Mangel, und ein Knopf, der nur da ist damit einer da ist, führt nirgendwohin. Beides respektiert prefers-reduced-motion. Nebenbei gefunden: `@keyframes spin` lag als Inline-<style> in **sechs** Komponenten, und zwei Nutzer (AuditTab, SearchBox) hatten gar keine eigene Kopie. Dass es lief, war Zufall – @keyframes gilt fürs ganze Dokument, es genügte also, dass irgendeine der sechs gerade im Bild war. Kaputt wäre es an dem Tag gewesen, an dem jemand die letzte Kopie entfernt. Steht jetzt einmal in index.css. Nicht angefasst: der Sprachmix. CalendarPage ist durchgehend deutsch, die übrige Oberfläche englisch, betroffen sind sechs Dateien. Eine halbe Übersetzung wäre schlimmer als keine – das gehört in einem Zug gemacht, samt der Entscheidung, welche Sprache die Oberfläche spricht. In TESTING.md notiert. Geprüft: tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen der sieben Selbsttest-Suiten grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import { useEffect } from 'react'
|
||
import { useAuth } from './hooks/useAuth'
|
||
import { AppSettingsProvider, useAppSettings } from './hooks/useAppSettings'
|
||
import { applyTheme } from './theme'
|
||
import LoginPage from './pages/LoginPage'
|
||
import Shell from './components/Shell'
|
||
import SharePage from './pages/SharePage'
|
||
|
||
// Simple path-based routing without react-router
|
||
function getShareToken(): string | null {
|
||
const match = window.location.pathname.match(/^\/s\/([^/]+)/)
|
||
return match ? match[1] : null
|
||
}
|
||
|
||
export default function App() {
|
||
const shareToken = getShareToken()
|
||
|
||
// Name und Farben umschliessen auch die Share-Seite: sie ist die einzige
|
||
// Ansicht, die Fremde zu Gesicht bekommen.
|
||
return (
|
||
<AppSettingsProvider>
|
||
{shareToken ? <SharePage token={shareToken} /> : <AuthedApp />}
|
||
</AppSettingsProvider>
|
||
)
|
||
}
|
||
|
||
function AuthedApp() {
|
||
const { user, loading, login, logout, refreshUser } = useAuth()
|
||
const { globalColors } = useAppSettings()
|
||
|
||
// Die persönliche Palette gilt nur, wenn der Nutzer sie eingeschaltet hat –
|
||
// sonst die des Administrators. Läuft bei jeder Änderung am Nutzer erneut,
|
||
// damit ein Umschalten im Profil sofort überall greift.
|
||
useEffect(() => {
|
||
if (!user) return
|
||
if (user.use_custom_theme) {
|
||
applyTheme(
|
||
user.primary_color || globalColors.primary,
|
||
user.accent_color || globalColors.accent,
|
||
)
|
||
} else {
|
||
applyTheme(globalColors.primary, globalColors.accent)
|
||
}
|
||
}, [user, globalColors])
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="aether-minvh" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<div style={{
|
||
width: 32, height: 32,
|
||
border: '2px solid var(--color-primary)',
|
||
borderTopColor: 'transparent',
|
||
borderRadius: '50%',
|
||
animation: 'spin 0.7s linear infinite',
|
||
}} />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!user) return <LoginPage onLogin={login} />
|
||
return <Shell user={user} onLogout={logout} onUserChanged={refreshUser} />
|
||
}
|