fix+feat: SPA routing, theme system, modern UI, setup wizard improvements

Fixes:
- Go server now serves index.html for all non-asset paths (SPA fallback)
- Null guard on data.documents prevents undefined.length TypeError
- App.vue waits for checkStatus() before rendering AppLayout (race condition)
- PWA manifest no longer references missing icon files
- index.html applies theme class before paint to prevent flash

Features:
- Theme store: dark/light toggle + 6 accent color presets + custom hex picker
- ThemeToggle component in header dropdown
- CSS custom properties for accent color (--accent-400/500/600/700)
- Tailwind accent-* color utilities driven by CSS vars
- SetupWizard: 3-step flow, password confirmation, LDAP test button,
  animated toggle, review step, proper error/success states
- Sidebar: loading skeleton, active page highlight, null-safe document list
- Global .input, .btn-primary, .btn-secondary, .card component classes
- Stub GraphQL responses for setup/login/testLdapConnection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 13:40:42 +02:00
parent 87821e8988
commit d93f3bb888
12 changed files with 799 additions and 146 deletions

View File

@@ -1,18 +1,28 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { gql } from '@/lib/gql'
import { useAppStore } from '@/stores/app'
const emit = defineEmits<{ close: [] }>()
interface DocMeta { slug: string; title: string }
const docs = ref<DocMeta[]>([])
const loading = ref(true)
const app = useAppStore()
const router = useRouter()
const route = useRoute()
onMounted(async () => {
const data = await gql<{ documents: DocMeta[] }>(`{ documents { slug title } }`)
docs.value = data.documents
try {
const data = await gql<{ documents?: DocMeta[] }>(`{ documents { slug title } }`)
docs.value = data.documents ?? []
} catch {
docs.value = []
} finally {
loading.value = false
}
})
function navigate(slug: string) {
@@ -22,38 +32,81 @@ function navigate(slug: string) {
</script>
<template>
<div class="flex flex-col h-full">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-700">
<router-link to="/" class="font-bold text-lg" @click="emit('close')">Archivum</router-link>
<button class="lg:hidden p-1 rounded hover:bg-surface-700" @click="emit('close')">
<div class="flex flex-col h-full select-none">
<!-- Logo / header -->
<div class="flex items-center justify-between px-4 py-4 border-b border-slate-200 dark:border-slate-700/60">
<router-link
to="/"
class="flex items-center gap-2 font-bold text-slate-900 dark:text-white text-lg"
@click="emit('close')"
>
<!-- Book icon -->
<svg class="w-6 h-6 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
Archivum
</router-link>
<button
class="btn-ghost p-1.5 lg:hidden"
@click="emit('close')"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Search placeholder -->
<div class="px-3 py-3 border-b border-slate-200 dark:border-slate-700/60">
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-400 text-sm cursor-not-allowed">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<span>Search</span>
<kbd class="ml-auto text-xs bg-slate-200 dark:bg-slate-700 rounded px-1.5 py-0.5">K</kbd>
</div>
</div>
<!-- Document tree -->
<nav class="flex-1 overflow-y-auto py-2">
<nav class="flex-1 overflow-y-auto py-2 px-2">
<div v-if="loading" class="space-y-1 px-2 mt-1">
<div v-for="i in 4" :key="i" class="h-7 rounded-md bg-slate-200 dark:bg-slate-700 animate-pulse" :style="{ width: `${60 + i * 8}%` }" />
</div>
<p v-else-if="docs.length === 0" class="text-xs text-slate-400 px-3 py-2">
No documents yet.
</p>
<button
v-for="doc in docs"
:key="doc.slug"
class="w-full text-left px-4 py-2 text-sm hover:bg-surface-700 truncate"
:class="[
'w-full text-left px-3 py-2 rounded-lg text-sm truncate transition',
route.params.slug === doc.slug
? 'bg-accent-500/10 text-accent-600 dark:text-accent-400 font-medium'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/60'
]"
@click="navigate(doc.slug)"
>
{{ doc.title }}
{{ doc.title || doc.slug }}
</button>
</nav>
<!-- New document button -->
<div class="p-3 border-t border-slate-700">
<!-- New document -->
<div class="p-3 border-t border-slate-200 dark:border-slate-700/60">
<router-link
to="/doc/new"
class="block w-full text-center py-2 rounded bg-blue-700 hover:bg-blue-600 text-sm font-medium"
class="btn-primary w-full text-center text-sm py-2"
@click="emit('close')"
>
+ New document
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
New document
</router-link>
</div>
</div>
</template>