fix: 1:1 prototype port – exact hms-* CSS, real logo, all components from app.js

This commit is contained in:
Implementation Engineer
2026-07-10 22:28:26 +02:00
parent 786cb0c040
commit 90a7d7f2e0
38 changed files with 2733 additions and 3250 deletions
+40 -93
View File
@@ -1,105 +1,52 @@
<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 -->
<footer class="hms-footer mt-16" role="contentinfo">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<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 class="flex items-center gap-2 mb-4" style="color: var(--text)">
<HmsLogo :size="36" />
<div><div class="font-bold text-sm">HMS Licht & Ton</div><div class="text-xs" style="color: var(--secondary)">Veranstaltungstechnik</div></div>
</div>
<p class="text-sm leading-relaxed" style="color: var(--secondary)">Hammerschmidt u. Mössle GbR seit über 20 Jahren Ihr Partner für professionelle Veranstaltungstechnik in der Region Leipheim / Ellzee.</p>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Navigation</h3>
<ul class="space-y-2 text-sm">
<li><NuxtLink to="/" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Home</NuxtLink></li>
<li><NuxtLink to="/referenzen" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Referenzen</NuxtLink></li>
<li><NuxtLink to="/mietkatalog" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Mietkatalog</NuxtLink></li>
<li><NuxtLink to="/kontakt" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Kontakt</NuxtLink></li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Kontakt</h3>
<ul class="space-y-2 text-sm" style="color: var(--secondary)">
<li>Grockelhofen 10, 89340 Leipheim</li>
<li><a href="tel:+498221204433" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">+49 (0) 8221 / 204433</a></li>
<li><a href="mailto:info@hms-licht-ton.de" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">info@hms-licht-ton.de</a></li>
<li class="flex gap-3 pt-2">
<a href="https://facebook.com" target="_blank" rel="noopener" aria-label="Facebook" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></a>
<a href="https://instagram.com" target="_blank" rel="noopener" aria-label="Instagram" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg></a>
</li>
</ul>
</div>
<div>
<h3 class="text-sm font-semibold mb-4" style="color: var(--text)">Rechtliches</h3>
<ul class="space-y-2 text-sm">
<li><NuxtLink to="/impressum" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">Impressum</NuxtLink></li>
<li><NuxtLink to="/datenschutz" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">DSGVO</NuxtLink></li>
<li><NuxtLink to="/agb-vermietung" style="color: var(--secondary)" class="hover:text-[var(--color-accent)]">AGB Vermietung</NuxtLink></li>
<li><NuxtLink to="/admin" style="color: var(--secondary)" class="hover:text-[var(--color-accent)] text-xs">Admin-Login</NuxtLink></li>
</ul>
</div>
</div>
<!-- Copyright -->
<div class="mt-8 pt-6 border-t border-border-default">
<p class="text-xs text-secondary text-center">
&copy; {{ new Date().getFullYear() }} Hammerschmidt u. M&ouml;ssle GbR &middot; Veranstaltungstechnik &middot; Leipheim / Ellzee
</p>
<div class="border-t mt-8 pt-6 text-center text-xs" :style="{ borderColor: 'var(--border)', color: 'var(--secondary)' }">
© {{ year }} Hammerschmidt u. Mössle GbR · Veranstaltungstechnik · Leipheim / Ellzee
</div>
</div>
</footer>
</template>
<script setup lang="ts">
// AppFooter 4-column footer with navigation, contact, legal links, and social icons
const year = new Date().getFullYear()
</script>
+47 -160
View File
@@ -1,178 +1,65 @@
<template>
<header
class="sticky top-0 z-50 bg-panel border-b border-border-default"
role="banner"
>
<header class="hms-header" 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 &amp; Ton
</span>
<div class="flex items-center justify-between" :style="{ height: 'var(--header-height)' }">
<NuxtLink to="/" class="flex items-center gap-2" style="color: var(--text)" aria-label="HMS Licht & Ton Startseite">
<HmsLogo :size="40" />
<div class="hidden sm:block leading-tight">
<div class="font-bold text-base" style="color: var(--text)">HMS Licht & Ton</div>
<div class="text-xs" style="color: var(--secondary)">Veranstaltungstechnik</div>
</div>
</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 class="hidden md:flex items-center gap-1" role="navigation" aria-label="Hauptnavigation">
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to"
class="hms-nav-btn px-4 py-2 rounded-lg text-sm font-medium"
:style="isActive(item.to) ? { color: 'var(--color-accent)', background: 'var(--color-accent-light)' } : { color: 'var(--text-muted)' }"
:aria-current="isActive(item.to) ? 'page' : undefined">
{{ item.label }}
</NuxtLink>
<NuxtLink to="/warenkorb" class="hms-btn hms-btn-primary text-sm ml-2 px-4 py-2">
<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="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Warenkorb
</NuxtLink>
</nav>
<!-- Desktop Right: Phone + Social + Cart -->
<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>
<!-- Cart Icon with Counter Badge -->
<button
class="relative flex items-center justify-center w-10 h-10 text-text-muted hover:text-accent transition-colors duration-fast min-h-44px"
aria-label="Warenkorb öffnen"
data-testid="header-cart-button"
@click="cartDrawerOpen = true"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
<span
v-if="cartCount > 0"
class="absolute -top-1 -right-1 bg-accent text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
data-testid="header-cart-count"
>{{ cartCount }}</span>
</button>
<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 Cart Button -->
<button
class="md:hidden relative flex items-center justify-center w-11 h-11 text-text-muted hover:text-accent transition-colors duration-fast"
aria-label="Warenkorb öffnen"
data-testid="header-cart-button-mobile"
@click="cartDrawerOpen = true"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
<span
v-if="cartCount > 0"
class="absolute -top-1 -right-1 bg-accent text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
data-testid="header-cart-count-mobile"
>{{ cartCount }}</span>
</button>
<!-- 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"/>
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden p-2 rounded-lg" :style="{ color: 'var(--text-muted)' }" :aria-expanded="mobileMenuOpen" aria-label="Menü öffnen/schließen">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</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>
<transition name="mobile-menu">
<nav v-if="mobileMenuOpen" id="mobile-menu" class="md:hidden border-t" :style="{ borderColor: 'var(--border)', background: 'var(--panel)' }" role="navigation" aria-label="Mobile Navigation">
<div class="px-4 py-3 space-y-1">
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to" @click="mobileMenuOpen = false"
class="hms-nav-btn block w-full text-left px-4 py-3 rounded-lg text-sm font-medium"
:style="isActive(item.to) ? { color: 'var(--color-accent)', background: 'var(--color-accent-light)' } : { color: 'var(--text-muted)' }">
{{ item.label }}
</NuxtLink>
<NuxtLink to="/warenkorb" @click="mobileMenuOpen = false" class="block w-full text-left px-4 py-3 rounded-lg text-sm font-medium text-white" style="background: var(--color-accent)">🛒 Warenkorb</NuxtLink>
</div>
</div>
</nav>
</nav>
</transition>
</header>
<!-- Cart Drawer -->
<CartDrawer v-model="cartDrawerOpen" />
</template>
<script setup lang="ts">
import { useCart } from "~/composables/useCart";
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const { totalCount: cartCount } = useCart();
const route = useRoute()
const mobileMenuOpen = ref(false)
const mobileMenuOpen = ref(false);
const cartDrawerOpen = ref(false);
const navItems = [
{ label: 'Home', to: '/' },
{ label: 'Referenzen', to: '/referenzen' },
{ label: 'Mietkatalog', to: '/mietkatalog' },
{ label: 'Kontakt', to: '/kontakt' }
]
function toggleMobileMenu(): void {
mobileMenuOpen.value = !mobileMenuOpen.value;
function isActive(to: string): boolean {
if (to === '/') return route.path === '/'
return route.path.startsWith(to)
}
function closeMobileMenu(): void {
mobileMenuOpen.value = false;
}
const route = useRoute();
watch(() => route.path, () => {
closeMobileMenu();
});
</script>
-179
View File
@@ -1,179 +0,0 @@
<template>
<Teleport to="body">
<!-- Backdrop -->
<Transition name="cart-backdrop">
<div
v-if="modelValue"
class="fixed inset-0 z-[60] bg-black/60"
data-testid="cart-drawer-backdrop"
@click="close"
/>
</Transition>
<!-- Drawer Panel -->
<Transition name="cart-drawer">
<aside
v-if="modelValue"
class="fixed z-[61] bg-panel border-l border-border-default shadow-xl flex flex-col"
:class="isMobile ? 'bottom-0 left-0 right-0 rounded-t-lg max-h-[80vh]' : 'top-0 right-0 bottom-0 w-96'"
role="dialog"
aria-label="Warenkorb"
data-testid="cart-drawer"
>
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-border-default shrink-0">
<h2 class="text-lg font-bold text-text" data-testid="cart-drawer-title">
Warenkorb
<span v-if="totalCount > 0" class="text-sm font-normal text-text-muted ml-1">({{ totalCount }})</span>
</h2>
<button
class="text-text-muted hover:text-accent transition-colors duration-fast w-8 h-8 flex items-center justify-center"
aria-label="Warenkorb schließen"
@click="close"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Empty State -->
<div v-if="isEmpty" class="flex-1 flex flex-col items-center justify-center px-6 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="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
</div>
<p class="text-text-muted text-sm mb-4">Ihr Warenkorb ist leer.</p>
<button class="btn-primary" @click="close">
Weiter einkaufen
</button>
</div>
<!-- Items List -->
<div v-else class="flex-1 overflow-y-auto px-4 py-3 space-y-3">
<div
v-for="item in items"
:key="item.equipment_id"
class="flex items-center gap-3 bg-surface rounded-md p-3"
data-testid="cart-drawer-item"
>
<!-- Item Image -->
<div class="w-12 h-12 rounded-md overflow-hidden bg-card shrink-0">
<img
v-if="item.image_url"
:src="item.image_url"
:alt="item.name"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full flex items-center justify-center">
<svg class="w-6 h-6 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="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 3.75h16.5a1.5 1.5 0 011.5 1.5v12a1.5 1.5 0 01-1.5 1.5H3.75a1.5 1.5 0 01-1.5-1.5V5.25a1.5 1.5 0 011.5-1.5z" />
</svg>
</div>
</div>
<!-- Item Info -->
<div class="flex-grow min-w-0">
<NuxtLink :to="`/mietkatalog/${item.equipment_id}`" class="text-sm font-medium text-text hover:text-accent transition-colors duration-fast line-clamp-1" @click="close">
{{ item.name }}
</NuxtLink>
<p class="text-xs text-text-muted">
{{ item.quantity }}×
<span v-if="item.rental_price != null">{{ formatPrice(item.rental_price) }} /Tag</span>
<span v-else>Preis auf Anfrage</span>
</p>
</div>
<!-- Remove Button -->
<button
class="text-text-muted hover:text-error transition-colors duration-fast w-8 h-8 flex items-center justify-center shrink-0"
:aria-label="`${item.name} entfernen`"
data-testid="cart-drawer-remove"
@click="removeItem(item.equipment_id)"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Footer -->
<div v-if="!isEmpty" class="shrink-0 border-t border-border-default px-4 py-3 space-y-3">
<NuxtLink to="/warenkorb" class="btn-secondary w-full" @click="close" data-testid="cart-drawer-view-cart">
Warenkorb ansehen
</NuxtLink>
<NuxtLink to="/mietanfrage" class="btn-primary w-full" @click="close" data-testid="cart-drawer-checkout">
Zur Mietanfrage
</NuxtLink>
</div>
</aside>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { useCart } from "~/composables/useCart";
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
"update:modelValue": [boolean];
}>();
const { items, totalCount, isEmpty, removeItem } = useCart();
const isMobile = ref(false);
function checkViewport(): void {
if (import.meta.client) {
isMobile.value = window.innerWidth < 768;
}
}
onMounted(() => {
checkViewport();
window.addEventListener("resize", checkViewport);
});
onUnmounted(() => {
if (import.meta.client) {
window.removeEventListener("resize", checkViewport);
}
});
function close(): void {
emit("update:modelValue", false);
}
function formatPrice(price: number | null): string {
if (price == null) return "—";
return new Intl.NumberFormat("de-DE", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price);
}
</script>
<style scoped>
.cart-backdrop-enter-active,
.cart-backdrop-leave-active {
transition: opacity 0.25s ease;
}
.cart-backdrop-enter-from,
.cart-backdrop-leave-to {
opacity: 0;
}
.cart-drawer-enter-active,
.cart-drawer-leave-active {
transition: transform 0.3s ease;
}
.cart-drawer-enter-from,
.cart-drawer-leave-to {
transform: translateX(100%);
}
</style>
+7 -24
View File
@@ -1,30 +1,13 @@
<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 class="text-center py-16 px-4" role="status">
<div class="text-6xl mb-4" aria-hidden="true">{{ icon }}</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="max-w-md mx-auto mb-6" style="color: var(--secondary)">{{ message }}</p>
<button v-if="actionLabel" @click="$emit('action')" class="hms-btn hms-btn-primary">{{ actionLabel }}</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string;
message: string;
ctaText?: string;
}>();
defineEmits<{
action: [];
}>();
withDefaults(defineProps<{ icon?: string; title: string; message: string; actionLabel?: string }>(), { icon: '🔍' })
defineEmits<{ action: [] }>()
</script>
+14 -79
View File
@@ -1,87 +1,22 @@
<template>
<article
class="card rounded-lg overflow-hidden border border-border-default hover:border-accent-border transition-colors duration-fast flex flex-col"
data-testid="equipment-card"
>
<!-- Image / Placeholder -->
<div class="relative w-full h-48 bg-surface overflow-hidden">
<img
v-if="equipment.image_url"
:src="equipment.image_url"
:alt="equipment.name"
class="w-full h-full object-cover"
loading="lazy"
/>
<div
v-else
class="w-full h-full flex items-center justify-center"
>
<svg class="w-12 h-12 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="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 3.75h16.5a1.5 1.5 0 011.5 1.5v12a1.5 1.5 0 01-1.5 1.5H3.75a1.5 1.5 0 01-1.5-1.5V5.25a1.5 1.5 0 011.5-1.5z" />
</svg>
</div>
<!-- Category Badge -->
<span
v-if="equipment.category"
class="absolute top-2 left-2 px-2 py-1 text-xs font-medium rounded-sm bg-bg/80 text-text-muted backdrop-blur-sm"
>
{{ equipment.category }}
</span>
<!-- Availability Badge -->
<span
v-if="!equipment.available"
class="absolute top-2 right-2 px-2 py-1 text-xs font-medium rounded-sm bg-error-bg text-error"
>
Nicht verfügbar
</span>
<div class="hms-eq-card" @click="$emit('click', item)" role="article" tabindex="0" @keydown.enter="$emit('click', item)">
<div class="aspect-[4/3] flex items-center justify-center relative overflow-hidden" style="background: var(--surface)">
<img v-if="item.image" :src="item.image" :alt="item.name" loading="lazy" class="absolute inset-0 w-full h-full object-cover" />
<div v-else class="text-4xl" style="color: var(--secondary)">📦</div>
<span v-if="item.category" class="hms-badge hms-badge-primary absolute top-3 left-3">{{ item.category }}</span>
</div>
<!-- Content -->
<div class="flex flex-col flex-grow p-4">
<h3 class="text-base font-bold text-text mb-1 line-clamp-1">
<NuxtLink :to="`/mietkatalog/${equipment.id}`" class="hover:text-accent transition-colors duration-fast">
{{ equipment.name }}
</NuxtLink>
</h3>
<p class="text-sm text-text-muted mb-3 line-clamp-2 flex-grow">
{{ equipment.description || "Keine Beschreibung verfügbar" }}
</p>
<!-- Price & Button -->
<div class="flex items-center justify-between mt-2">
<span v-if="equipment.rental_price != null" class="text-sm font-semibold text-accent">
{{ formatPrice(equipment.rental_price) }} /Tag
</span>
<span v-else class="text-sm text-text-muted">
Preis auf Anfrage
</span>
<button
class="btn-primary text-xs px-3 py-2"
@click="$emit('add-to-cart', { id: equipment.id, name: equipment.name })"
>
Mietanfrage
</button>
<div class="p-4">
<h3 class="font-semibold text-sm mb-1 truncate" style="color: var(--text)">{{ item.name }}</h3>
<p class="text-xs mb-3 line-clamp-2" style="color: var(--secondary)">{{ item.description }}</p>
<div class="flex items-center justify-between">
<span class="text-xs" style="color: var(--secondary)">{{ item.code }}</span>
<button @click.stop="$emit('add-to-cart', item)" class="hms-btn hms-btn-ghost text-xs px-3 py-1.5" style="color: var(--color-accent)" :aria-label="item.name + ' zum Warenkorb hinzufügen'">+ Hinzufügen</button>
</div>
</div>
</article>
</div>
</template>
<script setup lang="ts">
import type { EquipmentItem } from "~/composables/useEquipment";
const props = defineProps<{
equipment: EquipmentItem;
}>();
defineEmits<{
"add-to-cart": [{ id: number; name: string }];
}>();
function formatPrice(price: number | null): string {
if (price == null) return "—";
return new Intl.NumberFormat("de-DE", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price);
}
defineProps<{ item: Record<string, any> }>()
defineEmits<{ click: [item: any]; 'add-to-cart': [item: any] }>()
</script>
+8 -30
View File
@@ -1,37 +1,15 @@
<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 class="text-center py-16 px-4" role="alert">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-error-bg)', color: 'var(--color-error)' }">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 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>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="max-w-md mx-auto mb-6" style="color: var(--secondary)">{{ message }}</p>
<button @click="$emit('retry')" class="hms-btn hms-btn-primary">Erneut versuchen</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: [];
}>();
withDefaults(defineProps<{ title?: string; message: string }>(), { title: 'Ein Fehler ist aufgetreten' })
defineEmits<{ retry: [] }>()
</script>
+4 -62
View File
@@ -1,68 +1,10 @@
<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 &amp; TON</text>
<svg class="hms-logo-svg" viewBox="0 0 200 200" :style="`height:${size}px;width:auto`" aria-label="HMS Licht & Ton Logo" role="img">
<path fill="currentColor" d="M166.266,172.872h-37.687v-60.718H74.226v60.718H36.539V26.956h37.687v56.335h54.354V26.956h37.687V172.872z" />
<rect x="23.598" y="15.343" fill="none" stroke="#EC6925" stroke-width="15.1525" stroke-miterlimit="10" width="155.612" height="168.874" />
</svg>
</template>
<script setup lang="ts">
const accentColor: string = "#EC6925";
withDefaults(defineProps<{ size?: number }>(), { size: 40 })
</script>
-109
View File
@@ -1,109 +0,0 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
data-testid="lightbox"
@click.self="$emit('close')"
@keydown.esc="$emit('close')"
tabindex="0"
ref="lightboxRef"
>
<!-- Close Button -->
<button
class="absolute top-4 right-4 text-text text-3xl leading-none p-2 hover:text-accent transition-colors duration-fast"
data-testid="lightbox-close"
aria-label="Schließen"
@click="$emit('close')"
>
&times;
</button>
<!-- Prev Button -->
<button
class="absolute left-4 top-1/2 -translate-y-1/2 text-text text-3xl leading-none p-3 hover:text-accent transition-colors duration-fast"
data-testid="lightbox-prev"
aria-label="Vorheriges Bild"
@click="$emit('prev')"
>
&#8249;
</button>
<!-- Image -->
<div class="max-w-4xl max-h-[80vh] px-16">
<img
:src="image.src"
:alt="image.alt"
class="max-w-full max-h-[80vh] object-contain rounded-lg"
/>
<p class="text-center text-text-muted text-sm mt-4">{{ image.caption }}</p>
</div>
<!-- Next Button -->
<button
class="absolute right-4 top-1/2 -translate-y-1/2 text-text text-3xl leading-none p-3 hover:text-accent transition-colors duration-fast"
data-testid="lightbox-next"
aria-label="Nächstes Bild"
@click="$emit('next')"
>
&#8250;
</button>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from "vue";
interface LightboxImage {
src: string;
alt: string;
caption: string;
}
const props = defineProps<{
isOpen: boolean;
image: LightboxImage;
}>();
const lightboxRef = ref<HTMLElement | null>(null);
const emit = defineEmits<{
close: [];
prev: [];
next: [];
}>();
const handleKeydown = (e: KeyboardEvent) => {
if (!props.isOpen) return;
if (e.key === "Escape") {
emit("close");
} else if (e.key === "ArrowLeft") {
emit("prev");
} else if (e.key === "ArrowRight") {
emit("next");
}
};
onMounted(() => {
window.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
window.removeEventListener("keydown", handleKeydown);
});
watch(
() => props.isOpen,
(open) => {
if (open) {
document.body.style.overflow = "hidden";
nextTick(() => {
lightboxRef.value?.focus();
});
} else {
document.body.style.overflow = "";
}
}
);
</script>
+6 -18
View File
@@ -1,24 +1,12 @@
<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 class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" aria-live="polite" aria-busy="true">
<div v-for="i in count" :key="i" class="hms-card overflow-hidden">
<div class="hms-skeleton aspect-[4/3]" style="border-radius:0"></div>
<div class="p-4 space-y-3"><div class="hms-skeleton h-4 w-3/4"></div><div class="hms-skeleton h-3 w-full"></div><div class="hms-skeleton h-3 w-1/2"></div><div class="flex justify-between items-center pt-2"><div class="hms-skeleton h-3 w-20"></div><div class="hms-skeleton h-6 w-24" style="border-radius:var(--radius-md)"></div></div></div>
</div>
</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",
});
withDefaults(defineProps<{ count?: number }>(), { count: 6 })
</script>
+11
View File
@@ -0,0 +1,11 @@
<template>
<div class="hms-card p-6">
<div class="hms-icon-circle mb-4"><span aria-hidden="true">{{ icon }}</span></div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text)">{{ title }}</h3>
<p class="text-sm leading-relaxed" style="color: var(--text-muted)">{{ description }}</p>
</div>
</template>
<script setup lang="ts">
defineProps<{ icon: string; title: string; description: string }>()
</script>
+16 -82
View File
@@ -1,90 +1,24 @@
<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>
<span class="hms-speaker-icon-wrap" v-html="svgStr"></span>
</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,
});
import { computed } from 'vue'
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 props = withDefaults(defineProps<{ type?: string }>(), { type: 'linearray' })
const label = computed(() => props.label || labelMap[props.type] || props.type);
const icons: Record<string, string> = {
linearray: '<rect x="8" y="2" width="16" height="40" rx="2"/><circle cx="16" cy="10" r="3"/><circle cx="16" cy="22" r="4"/><circle cx="16" cy="34" r="3"/>',
subwoofer: '<rect x="4" y="8" width="24" height="20" rx="3"/><circle cx="16" cy="18" r="7"/><circle cx="16" cy="18" r="3"/>',
monitor: '<path d="M4 6h20v14H4z"/><path d="M8 24h12"/><circle cx="14" cy="13" r="3"/>',
pointsource: '<circle cx="16" cy="16" r="12"/><circle cx="16" cy="16" r="6"/><circle cx="16" cy="16" r="2"/>',
column: '<rect x="10" y="2" width="12" height="44" rx="2"/><circle cx="16" cy="8" r="2"/><circle cx="16" cy="16" r="2"/><circle cx="16" cy="24" r="2"/><circle cx="16" cy="32" r="2"/><circle cx="16" cy="40" r="2"/>',
movinghead: '<rect x="10" y="4" width="12" height="16" rx="2"/><path d="M16 20v8"/><rect x="8" y="28" width="16" height="4" rx="1"/>'
}
defineEmits<{
select: [type: string];
}>();
const svgStr = computed(() =>
'<svg class="hms-speaker-icon" viewBox="0 0 32 48" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">' +
(icons[props.type] || icons.linearray) +
'</svg>'
)
</script>