herosection erstellt
This commit is contained in:
BIN
public/video/black mit white stripes.webm
Normal file
BIN
public/video/black mit white stripes.webm
Normal file
Binary file not shown.
BIN
public/video/dark mit blue stripes.webm
Normal file
BIN
public/video/dark mit blue stripes.webm
Normal file
Binary file not shown.
BIN
public/video/türkis motherboard.webm
Normal file
BIN
public/video/türkis motherboard.webm
Normal file
Binary file not shown.
BIN
public/video/white_mit_black_stripes.webm
Normal file
BIN
public/video/white_mit_black_stripes.webm
Normal file
Binary file not shown.
@@ -1,7 +1,9 @@
|
||||
import { Component, signal, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Component, signal, OnInit } from '@angular/core';
|
||||
import { RouterOutlet, Router, NavigationEnd } from '@angular/router';
|
||||
import {provideIcons} from "@ng-icons/core";
|
||||
import { filter } from 'rxjs/operators';
|
||||
import {cssMenu} from "@ng-icons/css.gg";
|
||||
import { UmamiService } from '@core/services/umami.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -10,6 +12,19 @@ import {cssMenu} from "@ng-icons/css.gg";
|
||||
styleUrl: './app.scss',
|
||||
viewProviders: [provideIcons({cssMenu})]
|
||||
})
|
||||
export class App {
|
||||
export class App implements OnInit {
|
||||
protected readonly title = signal('hurler-webdesign-saas');
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private umami: UmamiService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd)
|
||||
).subscribe(() => {
|
||||
this.umami.trackPageview();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,27 @@ export class SeoService {
|
||||
private renderer =
|
||||
this.rendererFactory.createRenderer(null, null);
|
||||
|
||||
// Constants for metadata
|
||||
private readonly BRAND = "Hurler Webdesign";
|
||||
private readonly FALLBACK_IMAGE =
|
||||
'https://hurler-webdesign.de/assets/og-default.jpg';
|
||||
private readonly DEFAULT_TYPE = "website";
|
||||
|
||||
/**
|
||||
* Updates SEO metadata with the provided data.
|
||||
*
|
||||
* @param data - The SeoData object containing title, description, type, and image.
|
||||
* @param canonicalPath - Optional parameter for the canonical URL path.
|
||||
*/
|
||||
updateMetadata(data: SeoData, canonicalPath?: string) {
|
||||
const fullTitle = `${data.title} | ${this.BRAND}`;
|
||||
const url = `https://hurler-webdesign.de${canonicalPath || ""}`;
|
||||
|
||||
// Update title and meta description
|
||||
this.titleService.setTitle(fullTitle);
|
||||
this.metaService.updateTag({ name: "description", content: data.description });
|
||||
|
||||
// Open Graph
|
||||
// Open Graph metadata
|
||||
this.metaService.updateTag({
|
||||
property: "og:title",
|
||||
content: fullTitle,
|
||||
@@ -44,7 +52,7 @@ export class SeoService {
|
||||
});
|
||||
this.metaService.updateTag({ property: "og:url", content: url });
|
||||
|
||||
// Twitter
|
||||
// Twitter card metadata
|
||||
this.metaService.updateTag({
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image",
|
||||
@@ -63,13 +71,20 @@ export class SeoService {
|
||||
content: data.image || this.FALLBACK_IMAGE,
|
||||
});
|
||||
|
||||
// Update canonical URL if provided
|
||||
if (canonicalPath) {
|
||||
this.updateCanonicalUrl(url);
|
||||
}
|
||||
|
||||
// Set local business schema.org metadata
|
||||
this.setLocalBusinessSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the canonical URL for the given page.
|
||||
*
|
||||
* @param url - The new canonical URL.
|
||||
*/
|
||||
private updateCanonicalUrl(url: string) {
|
||||
let link: HTMLLinkElement =
|
||||
this.document.querySelector("link[rel='canonical']") ||
|
||||
@@ -81,7 +96,9 @@ export class SeoService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the local business schema.org JSON-LD script in the head of the document.
|
||||
*/
|
||||
private setLocalBusinessSchema() {
|
||||
const oldScript = this.document.getElementById('schema-org-data');
|
||||
if (oldScript) this.renderer.removeChild(this.document.head, oldScript);
|
||||
|
||||
16
src/app/core/services/umami.service.spec.ts
Normal file
16
src/app/core/services/umami.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UmamiService } from './umami.service';
|
||||
|
||||
describe('UmamiService', () => {
|
||||
let service: UmamiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UmamiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
42
src/app/core/services/umami.service.ts
Normal file
42
src/app/core/services/umami.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export interface UmamiEventData {
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
// Typdefinition für das globale umami-Objekt
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: {
|
||||
track: (eventName?: string, data?: UmamiEventData) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UmamiService {
|
||||
private get isAvailable(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.umami !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Trackt einen Pageview manuell – nötig bei SPAs wie Angular,
|
||||
* da kein echter Seitenaufruf stattfindet.
|
||||
*/
|
||||
trackPageview(): void {
|
||||
if (!this.isAvailable) return;
|
||||
window.umami!.track();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trackt ein benutzerdefiniertes Event.
|
||||
* @param eventName Name des Events, z.B. 'button-click'
|
||||
* @param data Optionale Zusatzdaten, z.B. { label: 'Hero CTA' }
|
||||
*/
|
||||
trackEvent(eventName: string, data?: UmamiEventData): void {
|
||||
if (!this.isAvailable) return;
|
||||
window.umami!.track(eventName, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
p {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
<p>footer works!</p>
|
||||
<footer>
|
||||
Hier kommt der Footer
|
||||
</footer>
|
||||
@@ -0,0 +1,3 @@
|
||||
footer {
|
||||
height: 500px;
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
<section class="hero-section">
|
||||
<h1 class="hero-section__header">
|
||||
<span>Digitales Handwerk</span> <br />
|
||||
<span>statt Standard-Baukasten</span>
|
||||
</h1>
|
||||
<p class="hero-section__claim">
|
||||
Wir programmieren Blitzschnelle, sichere und maßgeschneiderte Webseiten für kleine,
|
||||
mittelständische Unternehmen und Vereine. Ohne CMS-Ballast, dafür mit maximaler Performance.
|
||||
</p>
|
||||
<div class="hero-section__links">
|
||||
<button href="#" variant="primary" label="Leistungen ansehen">Leistungen ansehen</button>
|
||||
<button href="#" variant="secondary" label="Warum kein WordPress?">
|
||||
Warum kein WordPress?
|
||||
</button>
|
||||
<div class="hero-section__video-container">
|
||||
<video autoplay muted loop>
|
||||
<source src="/video/white_mit_black_stripes.webm" type="video/webm">
|
||||
</video>
|
||||
</div>
|
||||
<div class="hero-section__wrapper">
|
||||
<h1 class="hero-section__header">
|
||||
Digitales Handwerk <br />
|
||||
statt Standard-Baukasten
|
||||
</h1>
|
||||
<p class="hero-section__claim">
|
||||
Wir programmieren Blitzschnelle, sichere und maßgeschneiderte Webseiten für kleine,
|
||||
mittelständische Unternehmen und Vereine. Ohne CMS-Ballast, dafür mit maximaler Performance.
|
||||
</p>
|
||||
<div class="hero-section__links">
|
||||
<app-button [item]="{ label: 'Über uns', type: 'anchor', target: '#about' }" variant="primary"></app-button>
|
||||
<app-button (click)="onFeaturesClick()" [item]="{ label: 'Warum kein Wordpress', type: 'anchor', target: 'about'}"
|
||||
variant="primary"></app-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,4 +1,66 @@
|
||||
@use "abstracts";
|
||||
|
||||
.hero-section {
|
||||
position: relative; // WICHTIG: Bezugspunkt für das Video
|
||||
height: 100vh;
|
||||
margin-top: -60px;
|
||||
padding-top: 60px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center; // Zentriert den Text-Inhalt
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 100%;
|
||||
background-color: var(--overlay);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
@include abstracts.container-wrapper;
|
||||
}
|
||||
|
||||
&__video-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -2; // Hinter den Text legen
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-main); // Dein Wunsch-Style
|
||||
font-size: var(--font-size-xxl);
|
||||
position: relative; // Stellt sicher, dass der Text über dem Video-Layer bleibt
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
&__claim {
|
||||
color: var(--text-main);
|
||||
font-size: var(--font-size-xl);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
video {
|
||||
/* Das hier ist der entscheidende Teil */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; // WICHTIG: Füllt den Container komplett aus, ohne zu verzerren
|
||||
object-position: center; // Zentriert das Video, falls Ränder abgeschnitten werden
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { UmamiService } from '@core/services/umami.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero',
|
||||
imports: [],
|
||||
imports: [ButtonComponent],
|
||||
templateUrl: './hero.component.html',
|
||||
styleUrl: './hero.component.scss',
|
||||
})
|
||||
export class HeroComponent {
|
||||
constructor(private umami: UmamiService) {}
|
||||
|
||||
onFeaturesClick(): void {
|
||||
this.umami.trackEvent('features-anchor-click')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<section class="header">
|
||||
<div class="logo-container">
|
||||
<span class="logo-container__logo centered">H</span>
|
||||
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
|
||||
</div>
|
||||
<div class="header__nav-section centered">
|
||||
<div class="theme-toggle-container">
|
||||
<app-toogle-theme></app-toogle-theme>
|
||||
<div class="wrapper">
|
||||
<section class="header">
|
||||
<div class="logo-container">
|
||||
<span class="logo-container__logo centered">H</span>
|
||||
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
|
||||
</div>
|
||||
<app-nav-menu [items]="navigationService.landingNavigation()"></app-nav-menu>
|
||||
<div class="burger-menu centered">
|
||||
<ng-icon name="cssMenu" class="burger-menu__icon"></ng-icon>
|
||||
<div class="header__nav-section centered">
|
||||
<div class="theme-toggle-container">
|
||||
<app-toogle-theme></app-toogle-theme>
|
||||
</div>
|
||||
<app-nav-menu [items]="navigationService.landingNavigation()"></app-nav-menu>
|
||||
<div class="burger-menu centered">
|
||||
<ng-icon name="cssMenu" class="burger-menu__icon"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@@ -1,5 +1,15 @@
|
||||
@use "abstracts";
|
||||
|
||||
.wrapper {
|
||||
border-radius: 0 0 10px 10px;
|
||||
background-color: var(--nav-bg);
|
||||
backdrop-filter: var(--nav-backdrop);
|
||||
box-shadow: var(--nav-shadow);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-index-sticky);
|
||||
}
|
||||
|
||||
.header {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
<div class="landing-grid">
|
||||
<div class="grid-area-navigation">
|
||||
<app-navigation></app-navigation>
|
||||
</div>
|
||||
<div class="grid-area-hero" id="hero">
|
||||
<app-hero></app-hero>
|
||||
</div>
|
||||
<div class="grid-area-features" id="features-section">
|
||||
<app-features-section></app-features-section>
|
||||
</div>
|
||||
<div class="grid-area-footer">
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
</div>
|
||||
<app-navigation></app-navigation>
|
||||
<app-hero></app-hero>
|
||||
<app-features-section></app-features-section>
|
||||
<app-footer></app-footer>
|
||||
118
src/app/shared/ui/button/README_BUTTON.md
Normal file
118
src/app/shared/ui/button/README_BUTTON.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# NavButtonComponent
|
||||
|
||||
Eine Angular Standalone-Komponente die ein `NavigationItem` rendert und je nach `NavigationType` automatisch den passenden Link-Typ erzeugt.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Angular 17+
|
||||
- `RouterModule` im Projekt eingebunden
|
||||
|
||||
## Dateien
|
||||
|
||||
```
|
||||
button/
|
||||
├── button.component.ts
|
||||
├── button.component.html
|
||||
└── button.component.scss
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
Die Komponente ist standalone und wird direkt in den `imports` einer anderen Komponente oder eines Moduls eingebunden:
|
||||
|
||||
```typescript
|
||||
import { NavButtonComponent } from './nav-button/nav-button.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NavButtonComponent],
|
||||
// ...
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Typ | Pflicht | Default | Beschreibung |
|
||||
|-----------|--------------------------------------|---------|-------------|-------------------------------------|
|
||||
| `item` | `NavigationItem` | ✅ | — | Das NavigationItem-Objekt |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | ❌ | `'md'` | Größe des Buttons |
|
||||
| `variant` | `'primary' \| 'ghost' \| 'outline'` | ❌ | `'primary'` | Visueller Stil |
|
||||
| `disabled`| `boolean` | ❌ | `false` | Deaktiviert den Button |
|
||||
|
||||
## NavigationItem
|
||||
|
||||
```typescript
|
||||
export type NavigationType = "anchor" | "route" | "external";
|
||||
|
||||
export interface NavigationItem {
|
||||
readonly label: string; // Anzeigetext
|
||||
readonly type: NavigationType;
|
||||
readonly target: string; // Pfad, Anchor (#id) oder URL
|
||||
readonly icon?: string; // Optionales Icon (z. B. Emoji oder Icon-String)
|
||||
readonly children?: NavigationItem[];
|
||||
}
|
||||
```
|
||||
|
||||
## Verhalten je NavigationType
|
||||
|
||||
| Typ | Verhalten |
|
||||
|------------|----------------------------------------------------------------|
|
||||
| `route` | Rendert `<a [routerLink]>` mit `routerLinkActive` |
|
||||
| `anchor` | Rendert `<a href>` mit `scrollIntoView({ behavior: 'smooth'})` |
|
||||
| `external` | Rendert `<a target="_blank" rel="noopener noreferrer">` mit `↗`|
|
||||
|
||||
## Beispiele
|
||||
|
||||
```html
|
||||
<!-- Interner Routerlink -->
|
||||
<app-nav-button
|
||||
[item]="{ label: 'Home', type: 'route', target: '/home' }"
|
||||
/>
|
||||
|
||||
<!-- Anchor Scroll -->
|
||||
<app-nav-button
|
||||
[item]="{ label: 'Über uns', type: 'anchor', target: '#about' }"
|
||||
variant="outline"
|
||||
/>
|
||||
|
||||
<!-- Externer Link -->
|
||||
<app-nav-button
|
||||
[item]="{ label: 'GitHub', type: 'external', target: 'https://github.com', icon: '🐙' }"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<!-- Deaktiviert -->
|
||||
<app-nav-button
|
||||
[item]="{ label: 'Gesperrt', type: 'route', target: '/admin' }"
|
||||
[disabled]="true"
|
||||
/>
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
Alle Farben und Abstände sind über CSS Custom Properties steuerbar. Überschreibe die Tokens global in deinem `styles.scss`:
|
||||
|
||||
```scss
|
||||
:root {
|
||||
--btn-primary-bg: #1a1a2e;
|
||||
--btn-primary-color: #ffffff;
|
||||
--btn-primary-hover-bg: #16213e;
|
||||
|
||||
--btn-outline-border: #1a1a2e;
|
||||
--btn-outline-color: #1a1a2e;
|
||||
--btn-outline-hover-bg: #1a1a2e;
|
||||
--btn-outline-hover-color: #ffffff;
|
||||
|
||||
--btn-radius: 8px;
|
||||
--btn-font: 'Your Font', sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Aktiver Routerlink erhält die Klasse `nav-btn--active`
|
||||
- Deaktivierte Links erhalten `aria-disabled="true"`
|
||||
- Externe Links sind mit `aria-label="Opens in new tab"` gekennzeichnet
|
||||
- Focus-Styles über `:focus-visible` vorhanden
|
||||
@@ -1 +1,46 @@
|
||||
<p>button works!</p>
|
||||
<!-- Router Link (internal navigation) -->
|
||||
@if (isRouteType) {
|
||||
<a
|
||||
[routerLink]="item.target"
|
||||
routerLinkActive="nav-btn--active"
|
||||
[class]="hostClasses.join(' ')"
|
||||
[attr.aria-disabled]="disabled || null"
|
||||
>
|
||||
@if (item.icon) {
|
||||
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
|
||||
}
|
||||
<span class="nav-btn__label">{{ item.label }}</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- Anchor (smooth scroll) -->
|
||||
@if (isAnchorType) {
|
||||
<a
|
||||
[href]="item.target"
|
||||
(click)="scrollToAnchor($event)"
|
||||
[class]="hostClasses.join(' ')"
|
||||
[attr.aria-disabled]="disabled || null"
|
||||
>
|
||||
@if (item.icon) {
|
||||
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
|
||||
}
|
||||
<span class="nav-btn__label">{{ item.label }}</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<!-- External Link -->
|
||||
@if (isExternalType) {
|
||||
<a
|
||||
[href]="item.target"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
[class]="hostClasses.join(' ')"
|
||||
[attr.aria-disabled]="disabled || null"
|
||||
>
|
||||
@if (item.icon) {
|
||||
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
|
||||
}
|
||||
<span class="nav-btn__label">{{ item.label }}</span>
|
||||
<span class="nav-btn__external-icon" aria-label="Opens in new tab">↗</span>
|
||||
</a>
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// ─── Design Tokens ───────────────────────────────────────────────────────────
|
||||
:host {
|
||||
--btn-font: 'DM Mono', 'Courier New', monospace;
|
||||
--btn-radius: 4px;
|
||||
--btn-transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Size tokens
|
||||
--btn-sm-padding: 6px 14px;
|
||||
--btn-md-padding: 10px 22px;
|
||||
--btn-lg-padding: 14px 32px;
|
||||
--btn-sm-font: 0.75rem;
|
||||
--btn-md-font: 0.875rem;
|
||||
--btn-lg-font: 1rem;
|
||||
|
||||
// Color tokens — override at :root level to theme globally
|
||||
--btn-primary-bg: #0f0f0f;
|
||||
--btn-primary-color: #f5f5f5;
|
||||
--btn-primary-border: #0f0f0f;
|
||||
--btn-primary-hover-bg: #2a2a2a;
|
||||
|
||||
--btn-ghost-bg: transparent;
|
||||
--btn-ghost-color: #0f0f0f;
|
||||
--btn-ghost-border: transparent;
|
||||
--btn-ghost-hover-bg: rgba(0, 0, 0, 0.06);
|
||||
|
||||
--btn-outline-bg: transparent;
|
||||
--btn-outline-color: #0f0f0f;
|
||||
--btn-outline-border: #0f0f0f;
|
||||
--btn-outline-hover-bg: #0f0f0f;
|
||||
--btn-outline-hover-color: #f5f5f5;
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// ─── Base ─────────────────────────────────────────────────────────────────────
|
||||
.nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--btn-font);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
border: 1.5px solid;
|
||||
border-radius: var(--btn-radius);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
outline-offset: 3px;
|
||||
transition:
|
||||
background-color var(--btn-transition),
|
||||
color var(--btn-transition),
|
||||
border-color var(--btn-transition),
|
||||
box-shadow var(--btn-transition),
|
||||
transform var(--btn-transition);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
}
|
||||
|
||||
&:active:not(.nav-btn--disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
// ── Sizes ──────────────────────────────────────────────────────────────────
|
||||
&--sm {
|
||||
padding: var(--btn-sm-padding);
|
||||
font-size: var(--btn-sm-font);
|
||||
}
|
||||
|
||||
&--md {
|
||||
padding: var(--btn-md-padding);
|
||||
font-size: var(--btn-md-font);
|
||||
}
|
||||
|
||||
&--lg {
|
||||
padding: var(--btn-lg-padding);
|
||||
font-size: var(--btn-lg-font);
|
||||
}
|
||||
|
||||
// ── Variants ───────────────────────────────────────────────────────────────
|
||||
&--primary {
|
||||
background-color: var(--btn-primary-bg);
|
||||
color: var(--btn-primary-color);
|
||||
border-color: var(--btn-primary-border);
|
||||
|
||||
&:hover:not(.nav-btn--disabled) {
|
||||
background-color: var(--btn-primary-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
background-color: var(--btn-ghost-bg);
|
||||
color: var(--btn-ghost-color);
|
||||
border-color: var(--btn-ghost-border);
|
||||
|
||||
&:hover:not(.nav-btn--disabled) {
|
||||
background-color: var(--btn-ghost-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background-color: var(--btn-outline-bg);
|
||||
color: var(--btn-outline-color);
|
||||
border-color: var(--btn-outline-border);
|
||||
|
||||
&:hover:not(.nav-btn--disabled) {
|
||||
background-color: var(--btn-outline-hover-bg);
|
||||
color: var(--btn-outline-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
// ── States ─────────────────────────────────────────────────────────────────
|
||||
&--active {
|
||||
box-shadow: inset 0 0 0 1.5px currentColor;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ── Parts ──────────────────────────────────────────────────────────────────
|
||||
&__icon {
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__external-icon {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.65;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,55 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NavigationItem, isAnchor, isRoute } from '@core/models/navigation.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-button',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
templateUrl: './button.component.html',
|
||||
styleUrl: './button.component.scss',
|
||||
styleUrls: ['./button.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ButtonComponent {
|
||||
export class ButtonComponent implements OnInit {
|
||||
@Input({ required: true }) item!: NavigationItem;
|
||||
|
||||
/** Optional size variant */
|
||||
@Input() size: 'sm' | 'md' | 'lg' = 'md';
|
||||
|
||||
/** Optional visual variant */
|
||||
@Input() variant: 'primary' | 'ghost' | 'outline' = 'primary';
|
||||
|
||||
/** Whether the button is disabled */
|
||||
@Input() disabled = false;
|
||||
|
||||
isAnchorType = false;
|
||||
isRouteType = false;
|
||||
isExternalType = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isAnchorType = isAnchor(this.item);
|
||||
this.isRouteType = isRoute(this.item);
|
||||
this.isExternalType = this.item.type === 'external';
|
||||
}
|
||||
|
||||
/** Scroll to anchor target */
|
||||
scrollToAnchor(event: Event): void {
|
||||
event.preventDefault();
|
||||
const target = document.querySelector(this.item.target);
|
||||
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
get hostClasses(): string[] {
|
||||
return [
|
||||
'nav-btn',
|
||||
`nav-btn--${this.variant}`,
|
||||
`nav-btn--${this.size}`,
|
||||
this.disabled ? 'nav-btn--disabled' : '',
|
||||
].filter(Boolean);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<script defer src="https://stats.hurler-webdesign.de/script.js" data-website-id="33763c9b-43a8-4e36-a1c1-e07d31ed1e5b"></script>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
@use "abstracts";
|
||||
@use "base";
|
||||
@use "layout";
|
||||
@@ -13,3 +13,9 @@ $breakpoints: (
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin container-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
@@ -26,8 +26,7 @@
|
||||
--button-text: oklch(100% 0.01 250);
|
||||
--border: oklch(90% 0.02 250);
|
||||
--shadow-color: oklch(0% 0 250);
|
||||
|
||||
// Stapelbare Werte
|
||||
--overlay: oklch(90% 0.02 250 / 0.9); // Stapelbare Werte
|
||||
--z-index-sticky: 100;
|
||||
|
||||
// Skalierung (modulare Skala)
|
||||
@@ -41,6 +40,7 @@
|
||||
--font-size-base: clamp(1rem, 0.5vw + 0.875rem, 1.125rem);
|
||||
--font-size-lg: clamp(1.25rem, 1vw + 1rem, 1.5rem);
|
||||
--font-size-xl: clamp(1.5rem, 2vw + 1rem, 2.5rem);
|
||||
--font-size-xxl: clamp(2.5rem, 3vw + 1.5rem, 4.5rem)
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -54,6 +54,8 @@
|
||||
|
||||
--border: oklch(25% 0.02 250);
|
||||
--shadow-color: 0 0% 100%;
|
||||
--overlay: oklch(30% 0.0075 250 / 0.9);
|
||||
|
||||
}
|
||||
|
||||
// ==============================
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
@use "abstracts";
|
||||
|
||||
.landing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(auto, 1200px) 1fr;
|
||||
grid-template-areas:
|
||||
"navigation navigation navigation"
|
||||
"hero hero hero"
|
||||
". features-section ."
|
||||
"footer footer footer";
|
||||
.layout-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.grid-area-navigation {
|
||||
grid-area: navigation;
|
||||
border-radius: 0 0 10px 10px;
|
||||
background-color: var(--nav-bg);
|
||||
backdrop-filter: var(--nav-backdrop);
|
||||
box-shadow: var(--nav-shadow);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-index-sticky);
|
||||
}
|
||||
// .landing-grid {
|
||||
// display: grid;
|
||||
// grid-template-columns: 1fr minmax(auto, 1200px) 1fr;
|
||||
// grid-template-areas:
|
||||
// "navigation navigation navigation"
|
||||
// "hero hero hero"
|
||||
// ". features-section ."
|
||||
// "footer footer footer";
|
||||
// }
|
||||
|
||||
.grid-area-hero {
|
||||
grid-area: hero;
|
||||
margin-top: -60px;
|
||||
padding-top: 60px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 1)),
|
||||
linear-gradient(0.25turn, oklch(56.347% 0.24089 260.834 / 0.8), oklch(55.088% 0.23462 260.772 / 0.8), oklch(56.347% 0.24089 260.834 / 0.8)),
|
||||
url("/images/backgroundpattern.webp") center / cover no-repeat;
|
||||
}
|
||||
// .grid-area-navigation {
|
||||
// grid-area: navigation;
|
||||
|
||||
[data-theme="dark"] .grid-area-hero {
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.2)),
|
||||
linear-gradient(0.25turn, oklch(20.648% 0.14311 250 / 0.5), oklch(27.99% 0.10701 258.719 / 0.5), oklch(20.648% 0.14311 250 / 0.5)),
|
||||
url("/images/backgroundpattern.webp") center / cover no-repeat;
|
||||
}
|
||||
// }
|
||||
|
||||
.grid-area-features {
|
||||
grid-area: features-section;
|
||||
height: 80vh;
|
||||
}
|
||||
// .grid-area-hero {
|
||||
// grid-area: hero;
|
||||
// margin-top: -60px;
|
||||
// padding-top: 60px;
|
||||
// height: 100vh;
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// background:
|
||||
// linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 1)),
|
||||
// linear-gradient(0.25turn, oklch(56.347% 0.24089 260.834 / 0.8), oklch(55.088% 0.23462 260.772 / 0.8), oklch(56.347% 0.24089 260.834 / 0.8)),
|
||||
// url("/images/backgroundpattern.webp") center / cover no-repeat;
|
||||
// }
|
||||
|
||||
.grid-area-footer {
|
||||
grid-area: footer;
|
||||
}
|
||||
// [data-theme="dark"] .grid-area-hero {
|
||||
// background:
|
||||
// linear-gradient(rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.2)),
|
||||
// linear-gradient(0.25turn, oklch(20.648% 0.14311 250 / 0.5), oklch(27.99% 0.10701 258.719 / 0.5), oklch(20.648% 0.14311 250 / 0.5)),
|
||||
// url("/images/backgroundpattern.webp") center / cover no-repeat;
|
||||
// }
|
||||
|
||||
// .grid-area-features {
|
||||
// grid-area: features-section;
|
||||
// height: 80vh;
|
||||
// }
|
||||
|
||||
// .grid-area-footer {
|
||||
// grid-area: footer;
|
||||
// }
|
||||
@@ -1,2 +0,0 @@
|
||||
@forward "grid";
|
||||
@forward "header";
|
||||
Reference in New Issue
Block a user