T01: Frontend Foundation – Nuxt 3 + Tailwind + Design Tokens + Layout + Meta
- Nuxt 3 project with @nuxtjs/tailwindcss module - Tailwind CSS config with A0 Dark Theme design tokens (CSS custom properties) - AppHeader: sticky, logo, nav, phone, social icons, mobile burger menu - AppFooter: 4-column grid (Navigation, Kontakt, Rechtliches, Social) - HmsLogo: SVG with orange #EC6925 border - error.vue: 404 page with Seite nicht gefunden - robots.txt server route: Disallow: / - Meta: noindex, nofollow, noarchive, nosnippet - JSON-LD: LocalBusiness schema - OpenGraph tags - routeRules: SSR/CSR per route - Shared components: LoadingSkeleton, EmptyState, ErrorState, LegalContentPage, SpeakerIcon - 42 vitest tests passing - Build: 0 errors
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<footer
|
||||
class="bg-bg border-t border-border-default mt-auto"
|
||||
role="contentinfo"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<!-- Column 1: Navigation -->
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Navigation</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<NuxtLink to="/" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Home</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/referenzen" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Referenzen</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/mietkatalog" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Mietkatalog</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/kontakt" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Kontakt</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Kontakt -->
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Kontakt</h2>
|
||||
<address class="not-italic text-sm text-text-muted space-y-2">
|
||||
<p>Grockelhofen 10<br>89340 Leipheim</p>
|
||||
<p>
|
||||
<a href="tel:+498221204433" class="hover:text-accent transition-colors duration-fast">
|
||||
+49 (0) 8221 / 204433
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="mailto:info@hms-licht-ton.de" class="hover:text-accent transition-colors duration-fast">
|
||||
info@hms-licht-ton.de
|
||||
</a>
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
|
||||
<!-- Column 3: Rechtliches -->
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Rechtliches</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<NuxtLink to="/impressum" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">Impressum</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/datenschutz" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">DSGVO</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/agb-vermietung" class="text-text-muted hover:text-accent text-sm transition-colors duration-fast">AGB Vermietung</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Column 4: Social -->
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-text uppercase tracking-wider mb-4">Folgen Sie uns</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="https://www.facebook.com/hms.licht.ton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary hover:text-accent transition-colors duration-flex"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/hms.licht.ton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary hover:text-accent transition-colors duration-fast"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div class="mt-8 pt-6 border-t border-border-default">
|
||||
<p class="text-xs text-secondary text-center">
|
||||
© {{ new Date().getFullYear() }} Hammerschmidt u. Mössle GbR · Veranstaltungstechnik · Leipheim / Ellzee
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// AppFooter – 4-column footer with navigation, contact, legal links, and social icons
|
||||
</script>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-50 bg-panel border-b border-border-default"
|
||||
role="banner"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-14 lg:h-16">
|
||||
<!-- Logo -->
|
||||
<NuxtLink to="/" class="flex items-center gap-2 shrink-0" aria-label="HMS Licht & Ton Startseite">
|
||||
<HmsLogo />
|
||||
<span class="hidden sm:inline font-bold text-sm tracking-wide text-text">
|
||||
HMS Licht & Ton
|
||||
</span>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="hidden md:flex items-center gap-6" role="navigation" aria-label="Hauptnavigation">
|
||||
<NuxtLink to="/" class="nav-link">Home</NuxtLink>
|
||||
<NuxtLink to="/referenzen" class="nav-link">Referenzen</NuxtLink>
|
||||
<NuxtLink to="/mietkatalog" class="nav-link">Mietkatalog</NuxtLink>
|
||||
<NuxtLink to="/kontakt" class="nav-link">Kontakt</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Desktop Right: Phone + Social -->
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<a
|
||||
href="tel:+491726264796"
|
||||
class="flex items-center gap-1.5 text-text-muted hover:text-accent transition-colors duration-fast text-sm font-medium min-h-44px"
|
||||
aria-label="Telefon anrufen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h2.5a1 1 0 01.95.68l1.2 3.6a1 1 0 01-.27 1.02L7.5 9.5a12 12 0 005 5l1.2-1.13a1 1 0 011.02-.27l3.6 1.2a1 1 0 01.68.95V19a2 2 0 01-2 2A16 16 0 013 5z"/>
|
||||
</svg>
|
||||
<span>+49 172 6264796</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.facebook.com/hms.licht.ton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary hover:text-accent transition-colors duration-fast min-h-44px flex items-center"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.instagram.com/hms.licht.ton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-secondary hover:text-accent transition-colors duration-fast min-h-44px flex items-center"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Burger Button -->
|
||||
<button
|
||||
class="md:hidden flex items-center justify-center w-11 h-11 text-text-muted hover:text-accent transition-colors duration-fast"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
aria-controls="mobile-nav"
|
||||
aria-label="Menü öffnen/schließen"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
<svg v-if="!mobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6l12 12M18 6L6 18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<nav
|
||||
v-if="mobileMenuOpen"
|
||||
id="mobile-nav"
|
||||
class="md:hidden bg-panel border-t border-border-default"
|
||||
role="navigation"
|
||||
aria-label="Mobile Navigation"
|
||||
>
|
||||
<div class="px-4 py-3 space-y-1">
|
||||
<NuxtLink to="/" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Home</NuxtLink>
|
||||
<NuxtLink to="/referenzen" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Referenzen</NuxtLink>
|
||||
<NuxtLink to="/mietkatalog" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Mietkatalog</NuxtLink>
|
||||
<NuxtLink to="/kontakt" class="block nav-link px-3 py-3 min-h-44px" @click="closeMobileMenu">Kontakt</NuxtLink>
|
||||
|
||||
<div class="flex items-center gap-4 px-3 pt-4 pb-2 border-t border-border-default mt-2">
|
||||
<a href="tel:+491726264796" class="flex items-center gap-1.5 text-text-muted hover:text-accent text-sm min-h-44px" @click="closeMobileMenu">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 5a2 2 0 012-2h2.5a1 1 0 01.95.68l1.2 3.6a1 1 0 01-.27 1.02L7.5 9.5a12 12 0 005 5l1.2-1.13a1 1 0 011.02-.27l3.6 1.2a1 1 0 01.68.95V19a2 2 0 01-2 2A16 16 0 013 5z"/>
|
||||
</svg>
|
||||
<span>+49 172 6264796</span>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/hms.licht.ton" target="_blank" rel="noopener noreferrer" class="text-secondary hover:text-accent min-h-44px flex items-center" aria-label="Facebook" @click="closeMobileMenu">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.96.93-1.96 1.89v2.25h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/hms.licht.ton" target="_blank" rel="noopener noreferrer" class="text-secondary hover:text-accent min-h-44px flex items-center" aria-label="Instagram" @click="closeMobileMenu">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 01-1.38-.9 3.7 3.7 0 01-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.9 5.9 0 00-2.13 1.38A5.9 5.9 0 00.63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.9 5.9 0 001.38 2.13 5.9 5.9 0 002.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.9 5.9 0 002.13-1.38 5.9 5.9 0 001.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.9 5.9 0 00-1.38-2.13A5.9 5.9 0 0019.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0z"/>
|
||||
<path d="M12 5.84A6.16 6.16 0 1018.16 12 6.16 6.16 0 0012 5.84zm0 10.16A4 4 0 1116 12a4 4 0 01-4 4z"/>
|
||||
<circle cx="18.41" cy="5.59" r="1.44"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const mobileMenuOpen = ref(false);
|
||||
|
||||
function toggleMobileMenu(): void {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value;
|
||||
}
|
||||
|
||||
function closeMobileMenu(): void {
|
||||
mobileMenuOpen.value = false;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
watch(() => route.path, () => {
|
||||
closeMobileMenu();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-surface">
|
||||
<svg class="w-8 h-8 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-6a2 2 0 012-2h2a2 2 0 012 2v6m-6 0h6m-6 0H7m14 0a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2h12z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-text mb-2">{{ title }}</h2>
|
||||
<p class="text-text-muted text-sm mb-6 max-w-sm">{{ message }}</p>
|
||||
<button
|
||||
v-if="ctaText"
|
||||
class="btn-primary"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ ctaText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
message: string;
|
||||
ctaText?: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
action: [];
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<div class="w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-error-bg">
|
||||
<svg class="w-8 h-8 text-error" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M4.93 19h14.14c1.54 0 2.5-1.67 1.73-3L13.73 4c-.77-1.33-2.69-1.33-3.46 0L3.2 16c-.77 1.33.19 3 1.73 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-text mb-2">{{ title }}</h2>
|
||||
<p class="text-text-muted text-sm mb-6 max-w-sm">{{ message }}</p>
|
||||
<button
|
||||
v-if="retryText"
|
||||
class="btn-secondary"
|
||||
@click="$emit('retry')"
|
||||
>
|
||||
{{ retryText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
title?: string;
|
||||
message?: string;
|
||||
retryText?: string;
|
||||
}>(), {
|
||||
title: "Ein Fehler ist aufgetreten",
|
||||
message: "Bitte versuchen Sie es erneut.",
|
||||
retryText: "Erneut versuchen",
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
retry: [];
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="HMS Licht & Ton Logo"
|
||||
>
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="38"
|
||||
height="38"
|
||||
rx="4"
|
||||
:stroke="accentColor"
|
||||
stroke-width="2"
|
||||
fill="#1a1a1a"
|
||||
/>
|
||||
<text
|
||||
x="20"
|
||||
y="16"
|
||||
text-anchor="middle"
|
||||
fill="#ffffff"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="11"
|
||||
font-weight="800"
|
||||
>HMS</text>
|
||||
<path
|
||||
d="M8 22 L12 26 L8 30"
|
||||
:stroke="accentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M14 22 L18 26 L14 30"
|
||||
:stroke="accentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M20 22 L24 26 L20 30"
|
||||
:stroke="accentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<text
|
||||
x="20"
|
||||
y="36"
|
||||
text-anchor="middle"
|
||||
fill="#d4d4d4"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="5"
|
||||
font-weight="500"
|
||||
>LICHT & TON</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const accentColor: string = "#EC6925";
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="space-y-4">
|
||||
<div class="skeleton-shimmer rounded-md" style="height: 32px; width: 60%" />
|
||||
<div class="skeleton-shimmer rounded-md" style="height: 20px" />
|
||||
<div class="skeleton-shimmer rounded-md" style="height: 20px; width: 90%" />
|
||||
<div class="skeleton-shimmer rounded-md" style="height: 20px" />
|
||||
<div class="skeleton-shimmer rounded-md" style="height: 20px; width: 80%" />
|
||||
</div>
|
||||
|
||||
<!-- Content State -->
|
||||
<article v-else>
|
||||
<h1 class="text-3xl font-bold text-text mb-6">{{ title }}</h1>
|
||||
<div class="prose prose-invert max-w-none text-text-muted" v-html="content" />
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
title: string;
|
||||
content?: string;
|
||||
loading?: boolean;
|
||||
}>(), {
|
||||
content: "",
|
||||
loading: false,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="animate-pulse" :class="wrapperClass">
|
||||
<div
|
||||
v-for="n in count"
|
||||
:key="n"
|
||||
class="skeleton-shimmer rounded-md"
|
||||
:style="{ height: height, marginBottom: gap }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
count?: number;
|
||||
height?: string;
|
||||
gap?: string;
|
||||
wrapperClass?: string;
|
||||
}>(), {
|
||||
count: 3,
|
||||
height: "20px",
|
||||
gap: "12px",
|
||||
wrapperClass: "w-full",
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center gap-2 p-4 rounded-lg border border-border-default bg-panel hover:border-accent-border transition-colors duration-fast cursor-pointer group"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
:aria-label="type"
|
||||
@click="$emit('select', type)"
|
||||
@keydown.enter="$emit('select', type)"
|
||||
>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
class="text-secondary group-hover:text-accent transition-colors duration-fast"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<!-- Line Array -->
|
||||
<template v-if="type === 'line-array'">
|
||||
<rect x="6" y="16" width="8" height="16" rx="1" fill="currentColor" />
|
||||
<path d="M16 20 L24 14 M16 28 L24 34" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M26 12 L30 10 M26 36 L30 38" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.6" />
|
||||
<path d="M32 8 L36 6 M32 40 L36 42" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3" />
|
||||
</template>
|
||||
|
||||
<!-- Subwoofer -->
|
||||
<template v-else-if="type === 'subwoofer'">
|
||||
<rect x="8" y="8" width="32" height="32" rx="2" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<circle cx="24" cy="24" r="8" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<circle cx="24" cy="24" r="4" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
</template>
|
||||
|
||||
<!-- Point Source -->
|
||||
<template v-else-if="type === 'point-source'">
|
||||
<rect x="10" y="18" width="10" height="12" rx="1" fill="currentColor" />
|
||||
<path d="M22 16 Q28 24 22 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M26 12 Q34 24 26 36" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.6" />
|
||||
</template>
|
||||
|
||||
<!-- Column Array -->
|
||||
<template v-else-if="type === 'column-array'">
|
||||
<rect x="16" y="6" width="16" height="36" rx="1" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<circle cx="24" cy="14" r="3" fill="currentColor" />
|
||||
<circle cx="24" cy="24" r="3" fill="currentColor" />
|
||||
<circle cx="24" cy="34" r="3" fill="currentColor" />
|
||||
</template>
|
||||
|
||||
<!-- Monitor -->
|
||||
<template v-else-if="type === 'monitor'">
|
||||
<path d="M6 30 L24 14 L42 30 L42 34 L6 34 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||
<circle cx="24" cy="26" r="4" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
</template>
|
||||
|
||||
<!-- Moving Head -->
|
||||
<template v-else>
|
||||
<rect x="18" y="6" width="12" height="10" rx="1" fill="currentColor" />
|
||||
<path d="M24 16 L24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<circle cx="24" cy="30" r="6" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M18 30 L14 30 M30 30 L34 30" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.5" />
|
||||
</template>
|
||||
</svg>
|
||||
<span class="text-xs text-text-muted group-hover:text-accent transition-colors duration-fast">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
type: "line-array" | "subwoofer" | "point-source" | "column-array" | "monitor" | "moving-head";
|
||||
label?: string;
|
||||
size?: number;
|
||||
}>(), {
|
||||
label: "",
|
||||
size: 48,
|
||||
});
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
"line-array": "Line Array",
|
||||
"subwoofer": "Subwoofer",
|
||||
"point-source": "Point Source",
|
||||
"column-array": "Column Array",
|
||||
"monitor": "Monitor",
|
||||
"moving-head": "Moving Head",
|
||||
};
|
||||
|
||||
const label = computed(() => props.label || labelMap[props.type] || props.type);
|
||||
|
||||
defineEmits<{
|
||||
select: [type: string];
|
||||
}>();
|
||||
</script>
|
||||
Reference in New Issue
Block a user