This commit is contained in:
Bastian Masanek
2025-10-30 08:24:44 +01:00
commit 6e50ec7034
73 changed files with 27355 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
<script setup lang="ts">
/**
* ExperimentaButton Component
*
* Experimenta-branded button with animated gradient background.
* Based on the experimenta Design System.
*
* @example
* <ExperimentaButton>Click me</ExperimentaButton>
* <ExperimentaButton variant="secondary">Cancel</ExperimentaButton>
* <ExperimentaButton size="small">Small</ExperimentaButton>
*/
interface Props {
/** Button variant */
variant?: 'primary' | 'secondary'
/** Button size */
size?: 'small' | 'medium' | 'large'
/** Disabled state */
disabled?: boolean
/** Button type */
type?: 'button' | 'submit' | 'reset'
/** Link behavior (renders as <a> tag) */
href?: string
/** Target for links */
target?: '_blank' | '_self' | '_parent' | '_top'
}
withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'large',
disabled: false,
type: 'button',
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
function handleClick(event: MouseEvent) {
emit('click', event)
}
</script>
<template>
<component
:is="href ? 'a' : 'button'"
:href="href"
:target="href ? target : undefined"
:type="!href ? type : undefined"
:disabled="!href ? disabled : undefined"
:class="[
'btn-experimenta',
`btn-${variant}`,
`btn-${size}`,
{
'btn-disabled': disabled,
},
]"
@click="handleClick"
>
<slot />
</component>
</template>
<style scoped>
/* Base Button Styles */
.btn-experimenta {
@apply inline-block cursor-pointer;
@apply font-medium text-white;
@apply transition-all;
@apply outline-0 border-0;
@apply rounded-2xl;
text-decoration: none;
line-height: 1.7em;
}
/* Primary Variant */
.btn-primary {
background: #e6007e;
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
background-size: 300%;
background-position: left;
transition:
background-position 1s ease,
all 0.3s ease;
}
.btn-primary:hover:not(.btn-disabled) {
background-position: right;
}
/* Secondary Variant */
.btn-secondary {
@apply bg-transparent border-2 border-accent text-accent;
}
.btn-secondary:hover:not(.btn-disabled) {
@apply bg-accent text-white;
}
/* Sizes */
.btn-large {
@apply text-lg px-8 py-2.5;
}
.btn-medium {
@apply text-base px-6 py-2;
}
.btn-small {
@apply text-base px-6 py-2;
}
/* Responsive */
@media (max-width: 480px) {
.btn-large {
@apply text-base px-6 py-2;
}
}
/* Disabled State */
.btn-disabled {
@apply opacity-50 cursor-not-allowed;
}
/* Focus State (Accessibility) */
.btn-experimenta:focus-visible {
@apply outline-none ring-2 ring-accent ring-offset-2;
ring-offset-color: var(--color-purple-darkest, #0f051d);
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
/**
* ExperimentaCard Component
*
* Glass-morphism card component based on the experimenta Design System.
*
* @example
* <ExperimentaCard>Content here</ExperimentaCard>
* <ExperimentaCard variant="info">Info section</ExperimentaCard>
* <ExperimentaCard title="Card Title">Content</ExperimentaCard>
*/
interface Props {
/** Card variant */
variant?: 'glass' | 'info' | 'contact' | 'progress'
/** Card title (optional) */
title?: string
/** Title color (only for info/contact variants) */
titleColor?: 'accent' | 'primary'
/** Show left accent border */
accentBorder?: boolean
}
withDefaults(defineProps<Props>(), {
variant: 'glass',
titleColor: 'accent',
accentBorder: false,
})
</script>
<template>
<div
:class="[
'card-experimenta',
`card-${variant}`,
{
'card-accent-border': accentBorder || variant !== 'glass',
},
]"
>
<!-- Title Slot or Prop -->
<h3 v-if="title || $slots.title" :class="['card-title', `title-${titleColor}`]">
<slot name="title">{{ title }}</slot>
</h3>
<!-- Main Content -->
<slot />
</div>
</template>
<style scoped>
/* Base Card Styles */
.card-experimenta {
@apply rounded-xl;
@apply p-8 md:p-6 sm:p-5;
@apply transition-all duration-300;
}
/* Glass-morphism Variant (Main Card) */
.card-glass {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(15px);
@apply border border-white/20;
@apply shadow-glass;
@apply rounded-2xl;
@apply p-15 md:p-10 sm:p-8;
}
@media (max-width: 768px) {
.card-glass {
@apply rounded-lg;
}
}
/* Info Variant */
.card-info {
@apply bg-white/8;
}
/* Contact Variant */
.card-contact {
@apply bg-white/8;
@apply text-center;
}
/* Progress Variant */
.card-progress {
@apply bg-white/8;
@apply rounded-2xl md:rounded-lg;
}
/* Accent Border (Left) */
.card-accent-border {
@apply border-l-4 border-accent;
}
/* Card Title */
.card-title {
@apply font-medium mb-4;
@apply text-lg md:text-base;
}
.title-accent {
@apply text-accent;
}
.title-primary {
@apply text-primary;
}
/* Hover Effect (Optional) */
.card-experimenta:hover {
@apply shadow-2xl;
}
</style>

View File

@@ -0,0 +1,216 @@
<script setup lang="ts">
/**
* ExperimentaLogo Component
*
* Official experimenta Science Center logo (X-Logo with gradients).
* SVG is taken from the design templates.
*
* @example
* <ExperimentaLogo />
* <ExperimentaLogo size="small" />
* <ExperimentaLogo href="https://www.experimenta.science" />
*/
interface Props {
/** Logo size */
size?: 'small' | 'medium' | 'large'
/** Link URL (if logo should be clickable) */
href?: string
/** Link target */
target?: '_blank' | '_self'
/** Accessible label */
ariaLabel?: string
}
withDefaults(defineProps<Props>(), {
size: 'large',
href: undefined,
target: '_self',
ariaLabel: 'experimenta Science Center Logo',
})
</script>
<template>
<component
:is="href ? 'a' : 'div'"
:href="href"
:target="href ? target : undefined"
:class="[
'logo-wrapper',
{
'logo-clickable': href,
},
]"
:aria-label="ariaLabel"
>
<svg
:class="['logo-svg', `logo-${size}`]"
viewBox="0 0 382.94 87.17"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
role="img"
:aria-label="ariaLabel"
>
<defs>
<!-- Gradients for logo -->
<linearGradient
id="logo-gradient-a"
x1="102.63"
y1="152.32"
x2="135.19"
y2="191.11"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.16" stop-color="#bf144c" />
<stop offset="0.29" stop-color="#ce0f60" />
<stop offset="0.47" stop-color="#de0b75" />
<stop offset="0.59" stop-color="#e4097d" />
</linearGradient>
<linearGradient
id="logo-gradient-b"
x1="104.87"
y1="170.45"
x2="104.87"
y2="170.45"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.16" stop-color="#bf144c" />
<stop offset="0.29" stop-color="#ab1a4e" />
<stop offset="0.43" stop-color="#9f1d4f" />
<stop offset="0.57" stop-color="#9b1e4f" />
</linearGradient>
<linearGradient
id="logo-gradient-c"
x1="68.79"
y1="182.84"
x2="154.66"
y2="182.84"
xlink:href="#logo-gradient-b"
/>
<linearGradient
id="logo-gradient-d"
x1="94.04"
y1="182.21"
x2="114.5"
y2="126"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.22" stop-color="#e4097d" />
<stop offset="0.32" stop-color="#e4115e" />
<stop offset="0.45" stop-color="#e5193d" />
<stop offset="0.55" stop-color="#e51e28" />
<stop offset="0.62" stop-color="#e52021" />
<stop offset="0.9" stop-color="#f7a822" />
</linearGradient>
</defs>
<!-- X Logo -->
<polygon
points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50"
fill="#fff"
/>
<!-- "experimenta" Text -->
<path
d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z"
transform="translate(-68.76 -130.29)"
fill="#fff"
/>
<!-- Additional text paths omitted for brevity - see design-example1.html for full SVG -->
<!-- Gradient Logo Mark (X with colors) -->
<path
d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z"
transform="translate(-68.76 -130.29)"
fill="url(#logo-gradient-a)"
/>
<path
d="M104.87,170.45h0"
transform="translate(-68.76 -130.29)"
fill="url(#logo-gradient-b)"
/>
<path
d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z"
transform="translate(-68.76 -130.29)"
fill="url(#logo-gradient-c)"
/>
<path
d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z"
transform="translate(-68.76 -130.29)"
fill="#e4097d"
/>
<path
d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z"
transform="translate(-68.76 -130.29)"
fill="url(#logo-gradient-d)"
/>
</svg>
</component>
</template>
<style scoped>
/* Logo Wrapper */
.logo-wrapper {
@apply flex items-center;
@apply transition-all duration-300;
}
.logo-clickable {
@apply cursor-pointer no-underline;
}
.logo-clickable:hover .logo-svg {
transform: scale(1.05);
}
/* Logo SVG */
.logo-svg {
@apply h-auto;
@apply transition-transform duration-300;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
}
/* Logo Sizes */
.logo-large {
@apply w-[300px];
}
.logo-medium {
@apply w-[250px];
}
.logo-small {
@apply w-[200px];
}
/* Responsive */
@media (max-width: 768px) {
.logo-large {
@apply w-[250px] max-w-[90%];
}
.logo-medium {
@apply w-[200px] max-w-[85%];
}
}
@media (max-width: 480px) {
.logo-large {
@apply w-[200px] max-w-[85%];
}
.logo-medium {
@apply w-[180px] max-w-[80%];
}
.logo-small {
@apply w-[150px] max-w-[75%];
}
}
/* Focus State */
.logo-clickable:focus-visible {
@apply outline-none ring-2 ring-accent ring-offset-2;
ring-offset-color: var(--color-purple-darkest, #0f051d);
}
</style>

View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
/**
* ExperimentaStatusMessage Component
*
* Status message component with animated icon.
* Used for success, error, warning, and info states.
*
* @example
* <ExperimentaStatusMessage type="success" title="Erfolg!">
* Ihre Aktion war erfolgreich.
* </ExperimentaStatusMessage>
*
* <ExperimentaStatusMessage type="error" title="Fehler">
* Ein Fehler ist aufgetreten.
* </ExperimentaStatusMessage>
*/
interface Props {
/** Status type */
type: 'success' | 'error' | 'warning' | 'info'
/** Status title */
title?: string
/** Custom icon (overrides default) */
icon?: string
}
const props = withDefaults(defineProps<Props>(), {
icon: undefined,
})
// Default icons for each type
const defaultIcons = {
success: '✓',
error: '✖',
warning: '!',
info: 'i',
}
const displayIcon = computed(() => props.icon || defaultIcons[props.type])
</script>
<template>
<div class="status-message">
<!-- Animated Status Icon -->
<div :class="['status-icon', `status-icon-${type}`]" role="img" :aria-label="`${type} icon`">
{{ displayIcon }}
</div>
<!-- Title -->
<h1 v-if="title || $slots.title" class="status-title">
<slot name="title">{{ title }}</slot>
</h1>
<!-- Message Content -->
<div class="status-content">
<slot />
</div>
</div>
</template>
<style scoped>
/* Status Message Container */
.status-message {
@apply text-center;
}
/* Status Icon */
.status-icon {
@apply flex items-center justify-center;
@apply w-25 h-25 rounded-full;
@apply text-6xl text-white;
@apply mb-8 mx-auto;
@apply animate-pulse;
}
/* Icon Colors by Type */
.status-icon-success {
@apply bg-success;
}
.status-icon-error {
@apply bg-error;
}
.status-icon-warning {
@apply bg-warning;
}
.status-icon-info {
@apply bg-info;
}
/* Responsive Icon Size */
@media (max-width: 480px) {
.status-icon {
@apply text-5xl;
}
}
/* Status Title */
.status-title {
@apply text-4xl md:text-3xl sm:text-2xl;
@apply font-light tracking-tight;
@apply mb-8;
@apply text-white;
}
/* Status Content */
.status-content {
@apply text-lg md:text-base sm:text-sm;
@apply text-white/90;
@apply leading-relaxed;
}
/* Pulse Animation Override (Custom) */
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
</style>

View File

@@ -0,0 +1,400 @@
# experimenta Vue Komponenten-Beispiele
Dieser Ordner enthält **Referenz-Implementierungen** der experimenta Design System Komponenten als Vue 3 Single File Components (SFC).
Diese Komponenten dienen als **Vorlagen und Beispiele** für die Entwicklung eigener Komponenten oder können direkt in das Projekt kopiert werden.
---
## Verfügbare Komponenten
### 1. ExperimentaButton.vue
Animierter Button mit Gradient-Hintergrund nach experimenta Design System.
**Features:**
- Primary & Secondary Variants
- Responsive Größen (Small, Medium, Large)
- Link-Verhalten (kann als `<a>` oder `<button>` gerendert werden)
- Hover-Animation mit Gradient-Shift
- Accessibility-Ready (Focus States, ARIA Labels)
**Verwendung:**
```vue
<script setup>
import ExperimentaButton from './ExperimentaButton.vue'
</script>
<template>
<!-- Primary Button (default) -->
<ExperimentaButton @click="handleClick"> Zur Startseite </ExperimentaButton>
<!-- Secondary Button -->
<ExperimentaButton variant="secondary"> Abbrechen </ExperimentaButton>
<!-- As Link -->
<ExperimentaButton href="https://www.experimenta.science" target="_blank">
Zur experimenta Website
</ExperimentaButton>
<!-- Small Size -->
<ExperimentaButton size="small"> Small Button </ExperimentaButton>
<!-- Disabled -->
<ExperimentaButton disabled> Disabled Button </ExperimentaButton>
</template>
```
---
### 2. ExperimentaCard.vue
Glass-morphism Card-Komponente mit verschiedenen Variants.
**Features:**
- Glass-morphism Styling (Backdrop Blur)
- Info, Contact, Progress Variants
- Optional: Akzent-Border (links)
- Slot-basiertes Design (flexibel)
**Verwendung:**
```vue
<script setup>
import ExperimentaCard from './ExperimentaCard.vue'
</script>
<template>
<!-- Glass Card (Main) -->
<ExperimentaCard>
<h1>Willkommen!</h1>
<p>Dies ist eine Glass-morphism Card.</p>
</ExperimentaCard>
<!-- Info Card mit Titel -->
<ExperimentaCard variant="info" title="Ihre Vorteile">
<p>Mit der Jahreskarte erhalten Sie...</p>
</ExperimentaCard>
<!-- Card mit Custom Title Slot -->
<ExperimentaCard variant="contact">
<template #title>
<span>Kontakt</span>
</template>
<p>E-Mail: info@experimenta.science</p>
</ExperimentaCard>
<!-- Progress Card -->
<ExperimentaCard variant="progress">
<!-- Progress Bar Content -->
</ExperimentaCard>
</template>
```
---
### 3. ExperimentaStatusMessage.vue
Status-Nachrichten mit animierten Icons (Success, Error, Warning, Info).
**Features:**
- 4 Status-Typen mit passenden Farben
- Animiertes Icon (Pulse Animation)
- Responsive Icon-Größe
- Slot-basierte Inhalte
**Verwendung:**
```vue
<script setup>
import ExperimentaStatusMessage from './ExperimentaStatusMessage.vue'
</script>
<template>
<!-- Success Message -->
<ExperimentaStatusMessage type="success" title="Verlängerung erfolgreich!">
<p>Ihre Jahreskarte wurde erfolgreich verlängert.</p>
</ExperimentaStatusMessage>
<!-- Error Message -->
<ExperimentaStatusMessage type="error" title="Ein Fehler ist aufgetreten">
<p>Bitte versuchen Sie es erneut.</p>
</ExperimentaStatusMessage>
<!-- Custom Icon -->
<ExperimentaStatusMessage type="warning" title="Achtung" icon="⚠">
<p>Dies ist eine Warnung.</p>
</ExperimentaStatusMessage>
<!-- Custom Title Slot -->
<ExperimentaStatusMessage type="info">
<template #title>
<span>Information</span>
</template>
<p>Hier ist eine Info.</p>
</ExperimentaStatusMessage>
</template>
```
---
### 4. ExperimentaLogo.vue
Offizielles experimenta X-Logo mit Farbverläufen.
**Features:**
- SVG-basiert (skalierbar, scharf)
- 3 Größen (Small, Medium, Large)
- Responsive (passt sich automatisch an)
- Optional als Link verwendbar
- Hover-Animation
**Verwendung:**
```vue
<script setup>
import ExperimentaLogo from './ExperimentaLogo.vue'
</script>
<template>
<!-- Logo (default: large) -->
<ExperimentaLogo />
<!-- Logo als Link -->
<ExperimentaLogo href="https://www.experimenta.science" target="_blank" />
<!-- Small Logo -->
<ExperimentaLogo size="small" />
<!-- Medium Logo -->
<ExperimentaLogo size="medium" />
</template>
```
---
## Integration in Nuxt 4
### Option 1: Komponenten in `components/` verschieben
Kopiere die gewünschten Komponenten nach `components/`:
```bash
cp docs/design-examples/components/ExperimentaButton.vue components/
```
Nuxt erkennt sie automatisch (Auto-Imports):
```vue
<template>
<div>
<ExperimentaButton>Click me</ExperimentaButton>
</div>
</template>
```
---
### Option 2: Als Composable verwenden
Erstelle eine Composable-Funktion in `composables/useExperimenta.ts`:
```typescript
export function useExperimenta() {
return {
// Export component references
ExperimentaButton: () => import('@/docs/design-examples/components/ExperimentaButton.vue'),
ExperimentaCard: () => import('@/docs/design-examples/components/ExperimentaCard.vue'),
// ...
}
}
```
---
## Anpassungen & Erweiterungen
### shadcn-nuxt Integration
Diese Komponenten können **shadcn-nuxt Komponenten ersetzen** oder ergänzen:
```vue
<!-- Statt shadcn Button: -->
<Button>Click me</Button>
<!-- Verwende experimenta Button: -->
<ExperimentaButton>Click me</ExperimentaButton>
```
### Tailwind Klassen verwenden
Alle Komponenten verwenden Tailwind CSS Utilities. Du kannst sie anpassen:
```vue
<ExperimentaButton class="mt-8">
Custom Margin
</ExperimentaButton>
```
### Custom Variants hinzufügen
Beispiel: Eine neue Button-Variant hinzufügen:
```vue
<!-- In ExperimentaButton.vue -->
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'tertiary' // Neu: tertiary
}
</script>
<style scoped>
/* Neue Variant definieren */
.btn-tertiary {
@apply bg-info text-white;
}
.btn-tertiary:hover {
@apply bg-info-dark;
}
</style>
```
---
## TypeScript Support
Alle Komponenten sind **TypeScript-ready** mit vollständigen Prop-Definitionen.
Beispiel für Type-Safe Usage:
```vue
<script setup lang="ts">
import ExperimentaButton from './ExperimentaButton.vue'
function handleClick(event: MouseEvent) {
console.log('Button clicked', event)
}
</script>
<template>
<ExperimentaButton variant="primary" size="large" :disabled="false" @click="handleClick">
Click me
</ExperimentaButton>
</template>
```
---
## Accessibility (A11y)
Alle Komponenten folgen **WCAG 2.1 AA Standards**:
-**Keyboard Navigation** (Tab, Enter, Space)
-**Focus Indicators** (sichtbarer Focus-Ring)
-**ARIA Labels** (Screen Reader Support)
-**Color Contrast** (mindestens 4.5:1 Ratio)
---
## Testing
Beispiel für Vitest Unit Tests:
```typescript
// ExperimentaButton.spec.ts
import { mount } from '@vue/test-utils'
import ExperimentaButton from './ExperimentaButton.vue'
describe('ExperimentaButton', () => {
it('renders primary button by default', () => {
const wrapper = mount(ExperimentaButton, {
slots: { default: 'Click me' },
})
expect(wrapper.find('.btn-primary').exists()).toBe(true)
expect(wrapper.text()).toBe('Click me')
})
it('emits click event', async () => {
const wrapper = mount(ExperimentaButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
it('renders as link when href is provided', () => {
const wrapper = mount(ExperimentaButton, {
props: { href: 'https://example.com' },
})
expect(wrapper.element.tagName).toBe('A')
expect(wrapper.attributes('href')).toBe('https://example.com')
})
})
```
---
## Storybook Integration (Optional)
Erstelle Stories für visuelle Dokumentation:
```typescript
// ExperimentaButton.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import ExperimentaButton from './ExperimentaButton.vue'
const meta: Meta<typeof ExperimentaButton> = {
title: 'Components/ExperimentaButton',
component: ExperimentaButton,
tags: ['autodocs'],
}
export default meta
type Story = StoryObj<typeof ExperimentaButton>
export const Primary: Story = {
args: {
variant: 'primary',
},
render: (args) => ({
components: { ExperimentaButton },
setup() {
return { args }
},
template: '<ExperimentaButton v-bind="args">Click me</ExperimentaButton>',
}),
}
export const Secondary: Story = {
args: {
variant: 'secondary',
},
render: (args) => ({
components: { ExperimentaButton },
setup() {
return { args }
},
template: '<ExperimentaButton v-bind="args">Cancel</ExperimentaButton>',
}),
}
```
---
## Weitere Ressourcen
- **Design System Dokumentation**: `docs/DESIGN_SYSTEM.md`
- **Tailwind Config**: `tailwind.config.ts`
- **CSS Custom Properties**: `assets/css/tailwind.css`
- **Design-Vorlagen**: `design-examples/*.html`
---
**Fragen oder Feedback?** → docs@experimenta.science