Initial commit
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function Datenschutz() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Datenschutz</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80dvh;
|
||||
padding: 0 var(--space-4);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-background);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(12rem, 40vw, 28rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.05em;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 1px rgba(23, 23, 23, 0.06);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: var(--letter-spacing-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-soft);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-7);
|
||||
max-width: 38ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.cta {
|
||||
gap: var(--btn-gap);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrapper {
|
||||
padding: 0 var(--space-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import styles from "./error.module.css";
|
||||
import {
|
||||
ArrowCounterClockwiseIcon,
|
||||
ArrowLeftIcon,
|
||||
} from "@phosphor-icons/react/dist/ssr";
|
||||
|
||||
interface ErrorProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export default function Error({ error, reset }: ErrorProps) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main className={styles.wrapper}>
|
||||
<span className={styles.ghost} aria-hidden="true">
|
||||
500
|
||||
</span>
|
||||
|
||||
<div className={styles.content}>
|
||||
<p className={styles.label}>Serverfehler</p>
|
||||
<h1 className={styles.heading}>Etwas ist schiefgelaufen</h1>
|
||||
<p className={styles.description}>
|
||||
Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder
|
||||
komm später zurück.
|
||||
</p>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className={`btn btn--filled ${styles.cta}`}
|
||||
>
|
||||
<ArrowCounterClockwiseIcon className="btn-icon" size={18} />
|
||||
Erneut versuchen
|
||||
</button>
|
||||
|
||||
<Link href="/" className={`btn btn--outline ${styles.cta}`}>
|
||||
<ArrowLeftIcon className="btn-icon" size={18} />
|
||||
Zurück zur Startseite
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,58 @@
|
||||
@import "../styles/tokens.css";
|
||||
@import "../styles/buttons.css";
|
||||
@import "../styles/forms.css";
|
||||
@import "../styles/typography.css";
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: 112.5%; /* 16px × 1.125 = 18px */
|
||||
}
|
||||
html.has-menu-open {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--font-body-color);
|
||||
background: var(--color-background);
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
line-height: var(--line-height-body);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin-inline: auto;
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
main {
|
||||
padding: 0 var(--space-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><image width="1024" height="1024" xlink:href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function Impressum() {
|
||||
return (
|
||||
<main>
|
||||
<h1>impressum</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import CookieConsentBanner from "@/components/CookieConsent";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Manrope, Nunito_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
const nunitoSans = Nunito_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "700"],
|
||||
variable: "--font-body",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-heading",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
const siteName = "ENVU";
|
||||
const title = "ENVU - Code Connect";
|
||||
const description = "Wir bieten Ihnen maßgeschneiderte Lösungen.";
|
||||
const siteUrl = "https://envu.de";
|
||||
|
||||
const ogImage = {
|
||||
url: "/images/envu.png",
|
||||
alt: "Envu Wallpaper",
|
||||
width: 1200,
|
||||
height: 628,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: title,
|
||||
template: `%s | ${siteName}`,
|
||||
},
|
||||
description,
|
||||
keywords: "Envu, Hannover",
|
||||
authors: [{ name: siteName }],
|
||||
robots: "index, follow",
|
||||
alternates: {
|
||||
canonical: siteUrl,
|
||||
},
|
||||
appleWebApp: {
|
||||
title: siteName,
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName,
|
||||
locale: "de_DE",
|
||||
title,
|
||||
description,
|
||||
images: [ogImage],
|
||||
url: siteUrl,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@envu",
|
||||
title,
|
||||
description,
|
||||
images: [{ url: ogImage.url, alt: ogImage.alt }],
|
||||
},
|
||||
};
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
name: siteName,
|
||||
url: siteUrl,
|
||||
description,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: "Hannover",
|
||||
addressCountry: "DE",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="de" className={`${nunitoSans.variable} ${manrope.variable}`}>
|
||||
<body>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<CookieConsentBanner />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main>
|
||||
<p>Laden...</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "ENVU",
|
||||
"short_name": "ENVU",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80dvh;
|
||||
padding: 0 var(--space-4);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-background);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(12rem, 40vw, 28rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.05em;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 1px rgba(23, 23, 23, 0.06);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: var(--letter-spacing-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-soft);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--space-7);
|
||||
max-width: 38ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.cta {
|
||||
gap: var(--btn-gap);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.wrapper {
|
||||
padding: 0 var(--space-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import styles from "./not-found.module.css";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Seite nicht gefunden",
|
||||
description: "Die gesuchte Seite existiert nicht.",
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className={styles.wrapper}>
|
||||
<span className={styles.ghost} aria-hidden="true">
|
||||
404
|
||||
</span>
|
||||
|
||||
<div className={styles.content}>
|
||||
<p className={styles.label}>Fehler 404</p>
|
||||
<h1 className={styles.heading}>Seite nicht gefunden</h1>
|
||||
<p className={styles.description}>
|
||||
Die gesuchte Seite existiert leider nicht oder wurde verschoben.
|
||||
</p>
|
||||
|
||||
<Link href="/" className={`btn btn--filled ${styles.cta}`}>
|
||||
<ArrowLeftIcon className="btn-icon" size={18} />
|
||||
Zurück zur Startseite
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Startseite</h1>
|
||||
<h2>Startseite</h2>
|
||||
<h3>Startseite</h3>
|
||||
<h4>Startseite</h4>
|
||||
<p>Startseite</p>
|
||||
<br />
|
||||
<button className="btn btn--filled">Filled</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--outline">Outline</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--ghost">Ghost</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--tertiary">Tertiary</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--filled" disabled>
|
||||
Disabled
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--filled">
|
||||
<ArrowRightIcon className="btn-icon animate" size={18} />
|
||||
Jetzt starten
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--outline">
|
||||
<ArrowRightIcon className="btn-icon animate" size={18} />
|
||||
Outline
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--ghost">
|
||||
<ArrowRightIcon className="btn-icon animate" size={18} />
|
||||
Ghost
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--tertiary">
|
||||
<ArrowRightIcon className="btn-icon animate" size={18} />
|
||||
Tertiary
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<button className="btn btn--filled" disabled>
|
||||
<ArrowRightIcon className="btn-icon animate" size={18} />
|
||||
Disabled
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
<a href="#">Webdesign</a> und Webentwicklung bilden das Fundament jeder
|
||||
erfolgreichen digitalen Präsenz. Während Webdesign die visuelle
|
||||
Gestaltung, Benutzerführung und das Nutzererlebnis in den Vordergrund
|
||||
stellt, sorgt die Webentwicklung für die technische Umsetzung im
|
||||
Hintergrund. Beide Disziplinen greifen eng ineinander – ein
|
||||
ansprechendes Design entfaltet seine volle Wirkung nur durch sauberen,
|
||||
performanten Code.
|
||||
</p>
|
||||
<p>
|
||||
Moderne Websites müssen auf allen Geräten funktionieren, schnell laden
|
||||
und barrierefrei zugänglich sein. Suchmaschinenoptimierung, Ladezeiten
|
||||
und intuitive Navigation sind dabei keine optionalen Extras, sondern
|
||||
entscheidende Faktoren für den Erfolg. Wer heute im Netz wahrgenommen
|
||||
werden möchte, braucht mehr als nur eine schöne Oberfläche – er braucht
|
||||
eine durchdachte digitale Strategie, die Design und Technik vereint.
|
||||
</p>
|
||||
<br />
|
||||
<h2>Kontakt</h2>
|
||||
<form>
|
||||
{/* Name */}
|
||||
<div className="form-group">
|
||||
<label className="form-label form-label--required" htmlFor="name">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Max Mustermann"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* E-Mail */}
|
||||
<div className="form-group">
|
||||
<label className="form-label form-label--required" htmlFor="email">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="max@beispiel.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Telefon */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="telefon">
|
||||
Telefon
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="telefon"
|
||||
name="telefon"
|
||||
placeholder="+49 511 123456"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Betreff */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="betreff">
|
||||
Betreff
|
||||
</label>
|
||||
<select id="betreff" name="betreff">
|
||||
<option value="">Bitte auswählen</option>
|
||||
<option value="anfrage">Allgemeine Anfrage</option>
|
||||
<option value="angebot">Angebotsanfrage</option>
|
||||
<option value="support">Support</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Nachricht */}
|
||||
<div className="form-group">
|
||||
<label
|
||||
className="form-label form-label--required"
|
||||
htmlFor="nachricht"
|
||||
>
|
||||
Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
id="nachricht"
|
||||
name="nachricht"
|
||||
placeholder="Ihre Nachricht..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Datenschutz Checkbox */}
|
||||
<div className="form-group">
|
||||
<label className="form-check">
|
||||
<input type="checkbox" />
|
||||
<span className="form-check-label">
|
||||
Ich habe die Datenschutzerklärung gelesen und stimme zu.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Error-Beispiel zum Testen */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="fehler">
|
||||
Fehler-Beispiel
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fehler"
|
||||
className="is-error"
|
||||
defaultValue="Falscher Wert"
|
||||
/>
|
||||
<span className="form-error-msg">
|
||||
Bitte gib einen gültigen Wert ein.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Disabled-Beispiel */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="deaktiviert">
|
||||
Deaktiviert
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="deaktiviert"
|
||||
disabled
|
||||
defaultValue="Nicht bearbeitbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button type="submit" className="btn btn--filled">
|
||||
Nachricht senden
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = "https://deinedomain.de"; // ← hier deine Domain eintragen
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*", // gilt für alle Crawler (Google, Bing etc.)
|
||||
allow: "/", // alles erlaubt
|
||||
disallow: [
|
||||
"/api/", // API-Routen nicht indexieren
|
||||
"/admin/", // falls vorhanden: Admin-Bereich sperren
|
||||
"/datenschutz",
|
||||
"/impressum",
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: `${BASE_URL}/sitemap.xml`, // ← Sitemap verlinken
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// app/sitemap.ts
|
||||
// Erreichbar unter: https://deinedomain.de/sitemap.xml
|
||||
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = "https://deinedomain.de"; // ← Domain anpassen
|
||||
|
||||
// ─── Typen ───────────────────────────────────────────────────────────────────
|
||||
// interface BlogPost {
|
||||
// slug: string;
|
||||
// updatedAt: Date;
|
||||
// }
|
||||
|
||||
// ─── Daten laden (nur aktivieren wenn Blog vorhanden) ────────────────────────
|
||||
// async function getBlogPosts(): Promise<BlogPost[]> {
|
||||
// Hier API/CMS-Call, z.B.:
|
||||
// const res = await fetch(`${BASE_URL}/api/posts`);
|
||||
// return res.json();
|
||||
// }
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
// ─── Statische Seiten ──────────────────────────────────────────────────────
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: BASE_URL,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1.0,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/about`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/kontakt`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/datenschutz`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/impressum`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Dynamische Blog-Seiten (auskommentiert bis Blog vorhanden) ────────────
|
||||
// const posts = await getBlogPosts();
|
||||
// const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
|
||||
// url: `${BASE_URL}/blog/${post.slug}`,
|
||||
// lastModified: post.updatedAt,
|
||||
// changeFrequency: "weekly",
|
||||
// priority: 0.7,
|
||||
// }));
|
||||
|
||||
// ─── Alles zusammenführen ──────────────────────────────────────────────────
|
||||
return [
|
||||
...staticPages,
|
||||
// ...blogPages, ← aktivieren wenn Blog live ist
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
|
||||
export default function TestErrorPage() {
|
||||
throw new Error("Testfehler – Error-Seite prüfen");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function UeberUns() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Über Uns</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
:global(#cc-main) {
|
||||
/* Deine bestehenden Button-Overrides */
|
||||
--cc-font-family: var(--font-body);
|
||||
--cc-bg: #ffffff;
|
||||
--cc-btn-bg-primary: var(--btn-bg-primary);
|
||||
--cc-btn-bg-primary-hover: var(--btn-bg-primary-hover);
|
||||
--cc-btn-bg-primary-active: var(--btn-bg-primary-active);
|
||||
--cc-btn-text-light: var(--btn-text-light);
|
||||
--cc-btn-text-dark: var(--btn-text-dark);
|
||||
--cc-btn-bg-secondary: var(--btn-bg-ghost-hover);
|
||||
--cc-btn-bg-secondary-hover: var(--btn-bg-primary);
|
||||
--cc-btn-bg-secondary-text: var(--btn-text-dark);
|
||||
--cc-btn-border-radius: var(--btn-radius);
|
||||
--cc-modal-border-radius: 12px;
|
||||
--cc-primary-color: var(--font-body-color);
|
||||
--cc-toggle-on-bg: var(--btn-bg-primary);
|
||||
}
|
||||
|
||||
:global(#cc-main a),
|
||||
:global(#cc-main a:link),
|
||||
:global(#cc-main a:visited),
|
||||
:global(#cc-main a:hover),
|
||||
:global(#cc-main a:focus),
|
||||
:global(#cc-main a:active) {
|
||||
text-decoration: none !important;
|
||||
border-bottom: none !important;
|
||||
box-shadow: none !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
:global(#cc-main .cm) {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import * as CookieConsent from "vanilla-cookieconsent";
|
||||
import "vanilla-cookieconsent/dist/cookieconsent.css";
|
||||
import "./CookieConsent.module.css";
|
||||
|
||||
export default function CookieConsentBanner() {
|
||||
useEffect(() => {
|
||||
CookieConsent.run({
|
||||
categories: {
|
||||
necessary: {
|
||||
enabled: true,
|
||||
readOnly: true,
|
||||
},
|
||||
analytics: {},
|
||||
marketing: {},
|
||||
},
|
||||
language: {
|
||||
default: "de",
|
||||
translations: {
|
||||
de: {
|
||||
consentModal: {
|
||||
title: "Ihre Privatsphäre ist uns wichtig",
|
||||
description:
|
||||
"Wir verwenden Cookies, um Ihnen die bestmögliche Nutzung unserer Website zu ermöglichen. " +
|
||||
"Einige Cookies sind technisch notwendig, andere helfen uns, die Website zu verbessern " +
|
||||
"und Ihnen relevante Inhalte anzuzeigen. Sie können Ihre Auswahl jederzeit unter " +
|
||||
"<a href='/datenschutz'>Datenschutz</a> anpassen oder widerrufen.",
|
||||
acceptAllBtn: "Alle akzeptieren",
|
||||
acceptNecessaryBtn: "Nur notwendige",
|
||||
showPreferencesBtn: "Einstellungen anpassen",
|
||||
footer:
|
||||
"<a href='/impressum'>Impressum</a>" +
|
||||
"<a href='/datenschutz'>Datenschutzerklärung</a>",
|
||||
},
|
||||
preferencesModal: {
|
||||
title: "Cookie-Einstellungen",
|
||||
acceptAllBtn: "Alle akzeptieren",
|
||||
acceptNecessaryBtn: "Nur notwendige",
|
||||
savePreferencesBtn: "Auswahl speichern",
|
||||
closeIconLabel: "Schließen",
|
||||
sections: [
|
||||
{
|
||||
title: "Ihre Privatsphäre",
|
||||
description:
|
||||
"Wir setzen Cookies ein, um die Funktionalität unserer Website sicherzustellen " +
|
||||
"und unser Angebot kontinuierlich zu verbessern. Sie entscheiden selbst, " +
|
||||
"welche Kategorien Sie zulassen möchten.",
|
||||
},
|
||||
{
|
||||
title: "Technisch notwendig",
|
||||
description:
|
||||
"Diese Cookies sind für den reibungslosen Betrieb der Website unbedingt erforderlich " +
|
||||
"und können nicht deaktiviert werden. Sie speichern keine personenbezogenen Daten " +
|
||||
"und werden ausschließlich als Reaktion auf von Ihnen durchgeführte Aktionen gesetzt " +
|
||||
"(z. B. Spracheinstellungen oder Sicherheits-Token).",
|
||||
linkedCategory: "necessary",
|
||||
},
|
||||
{
|
||||
title: "Analyse & Statistik",
|
||||
description:
|
||||
"Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren. " +
|
||||
"Alle Daten werden anonymisiert gesammelt und dienen ausschließlich zur Verbesserung " +
|
||||
"unseres Angebots. Wir nutzen Google Analytics (GA4) – Ihre IP-Adresse wird dabei " +
|
||||
"anonymisiert und nicht gespeichert.",
|
||||
linkedCategory: "analytics",
|
||||
},
|
||||
{
|
||||
title: "Marketing & externe Medien",
|
||||
description:
|
||||
"Diese Cookies ermöglichen die Einbindung externer Inhalte wie YouTube-Videos, " +
|
||||
"Google Maps, Instagram-, Facebook- und X (Twitter)-Inhalte. " +
|
||||
"Ohne Ihre Zustimmung werden diese Inhalte nicht geladen und keine Daten übertragen. " +
|
||||
"Bitte beachten Sie, dass bei Aktivierung personenbezogene Daten " +
|
||||
"an Drittanbieter (ggf. auch in die USA) übertragen werden können.",
|
||||
linkedCategory: "marketing",
|
||||
},
|
||||
{
|
||||
title: "Weitere Informationen",
|
||||
description:
|
||||
"Bei Fragen zu unserer Cookie-Richtlinie wenden Sie sich bitte an uns über das " +
|
||||
"<a href='/kontakt'>Kontaktformular</a>. Mehr Details finden Sie in unserer " +
|
||||
"<a href='/datenschutz'>Datenschutzerklärung</a>.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
FOOTER
|
||||
Datei: components/Footer.module.css
|
||||
*/
|
||||
|
||||
:root {
|
||||
--footer-bg: #171717;
|
||||
--footer-text: #a1a1aa;
|
||||
--footer-text-hover: #ffffff;
|
||||
--footer-title: #ffffff;
|
||||
--footer-divider: rgba(255, 255, 255, 0.1);
|
||||
--footer-padding-y: clamp(3rem, 6vw, 5rem);
|
||||
--footer-gap: clamp(2rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
/* Wrapper */
|
||||
.footer {
|
||||
background-color: var(--footer-bg);
|
||||
color: var(--footer-text);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.footer__inner {
|
||||
max-width: 1280px;
|
||||
margin-inline: auto;
|
||||
padding: var(--footer-padding-y) clamp(1rem, 5vw, 2rem);
|
||||
}
|
||||
|
||||
/* Top */
|
||||
.footer__top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--footer-gap);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.footer__top {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.footer__top {
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand */
|
||||
.footer__logo {
|
||||
display: inline-block;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--footer-title);
|
||||
margin-bottom: 0.75rem;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.footer__logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer__tagline {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
max-width: 32ch;
|
||||
color: var(--footer-text);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.footer__nav {
|
||||
display: flex;
|
||||
gap: var(--footer-gap);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer__nav ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.footer__nav-group ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.footer__nav-title {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--footer-title);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer__nav-group a {
|
||||
font-size: 0.9rem;
|
||||
color: var(--footer-text);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer__nav-group a:hover {
|
||||
color: var(--footer-text-hover);
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.footer__divider {
|
||||
height: 1px;
|
||||
background: var(--footer-divider);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
/* Bottom */
|
||||
.footer__bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer__copy {
|
||||
font-size: 0.85rem;
|
||||
color: var(--footer-text);
|
||||
}
|
||||
|
||||
/* Social */
|
||||
.footer__social {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer__social a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--footer-divider);
|
||||
color: var(--footer-text);
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.footer__social a:hover {
|
||||
color: var(--footer-text-hover);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import styles from "./Footer.module.css";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
InstagramLogoIcon,
|
||||
LinkedinLogoIcon,
|
||||
XLogoIcon,
|
||||
FacebookLogoIcon,
|
||||
} from "@phosphor-icons/react/dist/ssr";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.footer__inner}>
|
||||
{/* Top: Brand + Navigation */}
|
||||
<div className={styles.footer__top}>
|
||||
{/* Brand */}
|
||||
<div className={styles.footer__brand}>
|
||||
<Link href="/" className={styles.footer__logo}>
|
||||
ENVU {/* ← Logo oder Bildkomponente hier */}
|
||||
</Link>
|
||||
<p className={styles.footer__tagline}>
|
||||
Maßgeschneiderte digitale Lösungen für Ihr Unternehmen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation Spalten */}
|
||||
<nav className={styles.footer__nav} aria-label="Footer Navigation">
|
||||
<div className={styles.footer__nav_group}>
|
||||
<span className={styles.footer__nav_title}>Unternehmen</span>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/ueberuns">Über uns</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/leistungen">Leistungen</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/kontakt">Kontakt</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer__nav_group}>
|
||||
<span className={styles.footer__nav_title}>Rechtliches</span>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/impressum">Impressum</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/datenschutz">Datenschutz</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className={styles.footer__divider} />
|
||||
|
||||
{/* Bottom: Copyright + Social */}
|
||||
<div className={styles.footer__bottom}>
|
||||
<p className={styles.footer__copy}>
|
||||
© {currentYear} ENVU. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
|
||||
<div className={styles.footer__social}>
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<InstagramLogoIcon size={20} />
|
||||
</a>
|
||||
<a
|
||||
href="https://linkedin.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<LinkedinLogoIcon size={20} />
|
||||
</a>
|
||||
<a
|
||||
href="https://x.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="X (Twitter)"
|
||||
>
|
||||
<XLogoIcon size={20} />
|
||||
</a>
|
||||
<a
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<FacebookLogoIcon size={20} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
:root {
|
||||
--header-bg: var(--color-surface);
|
||||
}
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background-color: var(--header-bg);
|
||||
box-shadow: 0 4px 6px -6px rgb(0 0 0 / 30%);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import styles from "./Header.module.css";
|
||||
import Navbar from "./Navbar/Navbar";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className={styles["header"]}>
|
||||
<Navbar />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
:root {
|
||||
--nav-max-width: var(--container-width);
|
||||
--nav-bg: var(--color-surface);
|
||||
--nav-padding-tb: 0;
|
||||
--nav-padding-lr: var(--space-4);
|
||||
--nav-font-size: 1rem;
|
||||
--nav-font-weight: 600;
|
||||
--nav-font-color: #5f5f5f;
|
||||
--nav-hamburger-icon: var(--nav-font-color);
|
||||
}
|
||||
.noTransition {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: var(--nav-padding-tb) 0 var(--nav-padding-tb) var(--nav-padding-lr);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: var(--nav-max-width);
|
||||
margin: 0 auto;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--nav-font-size) !important;
|
||||
font-weight: var(--nav-font-weight);
|
||||
background-color: var(--nav-bg);
|
||||
color: var(--nav-font-color);
|
||||
}
|
||||
.nav-logo {
|
||||
width: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-logo-image {
|
||||
width: auto;
|
||||
height: 30px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-links button:focus-visible {
|
||||
outline: 2px solid #000000;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
background-color: inherit;
|
||||
position: fixed;
|
||||
inset: var(--nav-h) 0 0 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition: max-height 400ms ease;
|
||||
/* Scrollbar ausblenden (Firefox/IE/Edge legacy) */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Chrome/Safari/Opera */
|
||||
.nav-links::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-links.open {
|
||||
max-height: 100vh;
|
||||
pointer-events: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-links li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.nav-links > li:first-child {
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.nav-links > li:last-child {
|
||||
padding: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.nav-links li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.nav-links li a:focus-visible {
|
||||
outline: 2px solid #393636;
|
||||
}
|
||||
|
||||
.nav-hamburger {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.nav-hamburger:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-hamburger:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px #000000;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nav-hamburger span {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background-color: var(--nav-hamburger-icon);
|
||||
border-radius: 2px;
|
||||
transform-origin: center;
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-hamburger span:nth-child(1) {
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% - 5px));
|
||||
}
|
||||
|
||||
.nav-hamburger span:nth-child(2) {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.nav-hamburger span:nth-child(3) {
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 5px));
|
||||
}
|
||||
|
||||
.nav-hamburger.open span:nth-child(1) {
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.nav-hamburger.open span:nth-child(2) {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) translateX(8px);
|
||||
}
|
||||
|
||||
.nav-hamburger.open span:nth-child(3) {
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--nav-padding-lr: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
:root {
|
||||
--nav-font-size: 16px;
|
||||
--nav-font-weight: 500;
|
||||
--nav-padding-lr: var(--space-6);
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: var(--nav-padding-tb) var(--nav-padding-lr) var(--nav-padding-tb)
|
||||
var(--nav-padding-lr);
|
||||
}
|
||||
|
||||
.nav-logo-image {
|
||||
width: auto;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
position: static;
|
||||
inset: auto;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: none;
|
||||
transition: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.nav-links > li a {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.nav-links > li:first-child {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-links > li:last-child {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-links.animating {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.nav-hamburger {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { endLinks, singleLinks, subMenus } from "@/data/navigation";
|
||||
import { useHeightVar } from "@/hooks/useHeightVar";
|
||||
import { useNavToggleMenu } from "@/hooks/useNavToggleMenu";
|
||||
import { useSupportsHover } from "@/hooks/useSupportsHover";
|
||||
import { useToggleBodyOverflow } from "@/hooks/useToggleBodyOverflow";
|
||||
import styles from "./Navbar.module.css";
|
||||
import { NavbarSubMenu } from "./NavbarSubMenu";
|
||||
|
||||
type BasicLink = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export default function Navbar() {
|
||||
const pathname = usePathname();
|
||||
const navRef = useRef<HTMLElement | null>(null);
|
||||
const supportsHover = useSupportsHover();
|
||||
|
||||
const [instantSwitch, setInstantSwitch] = useState(false);
|
||||
const raf1Ref = useRef<number | null>(null);
|
||||
const raf2Ref = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
menuOpen,
|
||||
openSubmenuId,
|
||||
noTransition,
|
||||
closeInstantNoAnim,
|
||||
setOpenSubmenuId,
|
||||
toggleMenu,
|
||||
} = useNavToggleMenu({ supportsHover, desktopBp: 1024 });
|
||||
|
||||
useToggleBodyOverflow(menuOpen);
|
||||
useHeightVar(navRef, "--nav-h", "offset");
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (raf1Ref.current !== null) cancelAnimationFrame(raf1Ref.current);
|
||||
if (raf2Ref.current !== null) cancelAnimationFrame(raf2Ref.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearInstantSwitchSoon = () => {
|
||||
if (raf1Ref.current !== null) cancelAnimationFrame(raf1Ref.current);
|
||||
if (raf2Ref.current !== null) cancelAnimationFrame(raf2Ref.current);
|
||||
|
||||
raf1Ref.current = requestAnimationFrame(() => {
|
||||
raf2Ref.current = requestAnimationFrame(() => {
|
||||
setInstantSwitch(false);
|
||||
raf1Ref.current = null;
|
||||
raf2Ref.current = null;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setOpenSubmenuIdWithInstantSwitch = (id: string | null) => {
|
||||
if (id !== null && openSubmenuId !== null && openSubmenuId !== id) {
|
||||
setInstantSwitch(true);
|
||||
setOpenSubmenuId(id);
|
||||
clearInstantSwitchSoon();
|
||||
return;
|
||||
}
|
||||
|
||||
setInstantSwitch(false);
|
||||
setOpenSubmenuId(id);
|
||||
};
|
||||
|
||||
const isActivePath = (to: string) => {
|
||||
if (to === "/") return pathname === "/";
|
||||
return pathname === to || pathname.startsWith(`${to}/`);
|
||||
};
|
||||
|
||||
const handleNavLinkClick = () => {
|
||||
setInstantSwitch(false);
|
||||
closeInstantNoAnim();
|
||||
};
|
||||
|
||||
const renderNavLink = (link: BasicLink) => (
|
||||
<li key={link.to + link.label}>
|
||||
<Link
|
||||
href={link.to}
|
||||
onClick={handleNavLinkClick}
|
||||
aria-current={isActivePath(link.to) ? "page" : undefined}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className={styles["nav"]} ref={navRef} aria-label="Hauptnavigation">
|
||||
<div className={styles["nav-logo"]}>
|
||||
<Link href="/" aria-label="Startseite">
|
||||
<Image
|
||||
src="/logo/logo.svg"
|
||||
alt="Logo"
|
||||
width={160}
|
||||
height={48}
|
||||
priority
|
||||
className={styles["nav-logo-image"]}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
id="main-navigation"
|
||||
className={[
|
||||
styles["nav-links"],
|
||||
menuOpen ? styles.open : "",
|
||||
noTransition ? styles.noTransition : "",
|
||||
].join(" ")}
|
||||
>
|
||||
{singleLinks.map(renderNavLink)}
|
||||
|
||||
{subMenus.map((menu) => (
|
||||
<NavbarSubMenu
|
||||
key={menu.id}
|
||||
id={menu.id}
|
||||
label={menu.label}
|
||||
items={menu.items}
|
||||
open={openSubmenuId === menu.id}
|
||||
noTransition={noTransition}
|
||||
instantSwitch={instantSwitch}
|
||||
setOpenSubmenuId={setOpenSubmenuIdWithInstantSwitch}
|
||||
closeInstantNoAnim={handleNavLinkClick}
|
||||
/>
|
||||
))}
|
||||
|
||||
{endLinks.map(renderNavLink)}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles["nav-hamburger"]} ${menuOpen ? styles.open : ""}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label={menuOpen ? "Menü schließen" : "Menü öffnen"}
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="main-navigation"
|
||||
>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
:root {
|
||||
--nav-submenu-max-width: var(--container-width);
|
||||
--nav-submenu-bg: var(--color-surface);
|
||||
}
|
||||
.nav-submenu {
|
||||
display: grid;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
max-height: 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition: max-height 250ms ease;
|
||||
background-color: var(--nav-bg);
|
||||
}
|
||||
|
||||
.nav-submenu.open {
|
||||
max-height: var(--submenu-h);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nav-submenu-inner {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.nav-submenu-list {
|
||||
width: var(--nav-submenu-max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-sub-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background-color: inherit;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.nav-item-with-sub {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-submenu > li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-submenu > li a {
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.nav-submenu {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--nav-h);
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 6px -6px rgb(0 0 0 / 30%);
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-height 300ms,
|
||||
opacity 300ms;
|
||||
}
|
||||
|
||||
.nav-submenu.open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-submenu > li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-submenu > li a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-submenu-list {
|
||||
width: min(100%, var(--nav-submenu-max-width));
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-rows: repeat(5, auto);
|
||||
grid-auto-columns: minmax(180px, max-content);
|
||||
column-gap: 48px;
|
||||
row-gap: 4px;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
padding: 24px 24px 48px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-sub-toggle {
|
||||
position: relative;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, MouseEvent } from "react";
|
||||
import { useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useHeightVar } from "@/hooks/useHeightVar";
|
||||
import { useIsDesktop } from "@/hooks/useIsDesktop";
|
||||
|
||||
import styles from "./NavbarSubMenu.module.css";
|
||||
|
||||
type SubMenuItem = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
open: boolean;
|
||||
noTransition: boolean;
|
||||
instantSwitch: boolean;
|
||||
setOpenSubmenuId: (id: string | null) => void;
|
||||
closeInstantNoAnim?: () => void;
|
||||
label: string;
|
||||
items: SubMenuItem[];
|
||||
};
|
||||
|
||||
export const NavbarSubMenu: FC<Props> = ({
|
||||
id,
|
||||
open,
|
||||
noTransition,
|
||||
instantSwitch,
|
||||
setOpenSubmenuId,
|
||||
closeInstantNoAnim,
|
||||
label,
|
||||
items,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const subMenuRef = useRef<HTMLUListElement | null>(null);
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
const openMenu = () => setOpenSubmenuId(id);
|
||||
const closeMenu = () => setOpenSubmenuId(null);
|
||||
const toggleMenu = () => setOpenSubmenuId(open ? null : id);
|
||||
|
||||
useHeightVar(subMenuRef, "--submenu-h", "scroll", open);
|
||||
|
||||
const handleSubmenuClick = (e: MouseEvent<HTMLUListElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest("a")) {
|
||||
closeInstantNoAnim?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
className={styles["nav-item-with-sub"]}
|
||||
onMouseEnter={isDesktop ? openMenu : undefined}
|
||||
onMouseLeave={isDesktop ? closeMenu : undefined}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles["nav-sub-toggle"]}
|
||||
onClick={toggleMenu}
|
||||
aria-expanded={open}
|
||||
aria-controls={`submenu-${id}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
<ul
|
||||
ref={subMenuRef}
|
||||
id={`submenu-${id}`}
|
||||
className={[
|
||||
styles["nav-submenu"],
|
||||
open ? styles.open : "",
|
||||
noTransition || instantSwitch ? styles.noTransition : "",
|
||||
].join(" ")}
|
||||
onClick={handleSubmenuClick}
|
||||
>
|
||||
<li className={styles["nav-submenu-inner"]}>
|
||||
<ul className={styles["nav-submenu-list"]}>
|
||||
{items.map((item) => {
|
||||
const isActive =
|
||||
item.to === "/"
|
||||
? pathname === "/"
|
||||
: pathname === item.to || pathname.startsWith(`${item.to}/`);
|
||||
|
||||
return (
|
||||
<li key={item.to}>
|
||||
<Link
|
||||
href={item.to}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
onClick={closeInstantNoAnim}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
export type NavItem = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type SubMenu = {
|
||||
id: string;
|
||||
label: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
export const singleLinks: NavItem[] = [{ label: "Home", to: "/" }];
|
||||
|
||||
export const subMenus: SubMenu[] = [
|
||||
{
|
||||
id: "services",
|
||||
label: "Dienstleistungen",
|
||||
items: [
|
||||
{ label: "Überblick", to: "/dienstleistungen" },
|
||||
{ label: "Zweiter Link", to: "/dienstleistungen/zweiter-link" },
|
||||
{ label: "Dritter Link", to: "/dienstleistungen/Dritter Link" },
|
||||
{ label: "Produkt 5", to: "/produkte/produkt-5" },
|
||||
{ label: "Produkt 6", to: "/produkte/produkt-6" },
|
||||
{ label: "Produkt 7", to: "/produkte/produkt-7" }
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "products",
|
||||
label: "Produkte",
|
||||
items: [
|
||||
{ label: "Produkt 1", to: "/produkte" },
|
||||
{ label: "Produkt 2", to: "/produkte/produkt-2" },
|
||||
{ label: "Produkt 3", to: "/produkte/produkt-3" },
|
||||
{ label: "Produkt 4", to: "/produkte/produkt-4" },
|
||||
{ label: "Produkt 5", to: "/produkte/produkt-5" },
|
||||
{ label: "Produkt 6", to: "/produkte/produkt-6" },
|
||||
{ label: "Produkt 7", to: "/produkte/produkt-7" },
|
||||
{ label: "Produkt 8", to: "/produkte/produkt-8" },
|
||||
{ label: "Produkt 9", to: "/produkte/produkt-9" },
|
||||
{ label: "Produkt 10", to: "/produkte/produkt-10" },
|
||||
{ label: "Produkt 11", to: "/produkte/produkt-11" },
|
||||
{ label: "Produkt 12", to: "/produkte/produkt-12" },
|
||||
{ label: "Produkt 13", to: "/produkte/produkt-13" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const endLinks: NavItem[] = [
|
||||
{ label: "Branche", to: "/branche" },
|
||||
{ label: "Kontakt", to: "/kontakt" },
|
||||
{ label: "Blog", to: "/blog" },
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
type Mode = "scroll" | "offset";
|
||||
|
||||
export function useHeightVar(
|
||||
ref: React.RefObject<HTMLElement | null>,
|
||||
cssVarName: string,
|
||||
mode: Mode = "scroll",
|
||||
trigger?: unknown,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const measure = () => {
|
||||
const h = mode === "scroll" ? el.scrollHeight : el.offsetHeight;
|
||||
el.style.setProperty(cssVarName, `${h}px`);
|
||||
};
|
||||
|
||||
measure();
|
||||
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(el);
|
||||
|
||||
return () => ro.disconnect();
|
||||
}, [ref, cssVarName, mode, trigger]);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useIsDesktop(breakpoint = 1024) {
|
||||
const [isDesktop, setIsDesktop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(`(min-width: ${breakpoint}px)`);
|
||||
|
||||
const update = () => setIsDesktop(mql.matches);
|
||||
|
||||
update();
|
||||
|
||||
if (mql.addEventListener) {
|
||||
mql.addEventListener("change", update);
|
||||
return () => mql.removeEventListener("change", update);
|
||||
}
|
||||
|
||||
mql.addListener(update);
|
||||
return () => mql.removeListener(update);
|
||||
}, [breakpoint]);
|
||||
|
||||
return isDesktop;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type Options = {
|
||||
supportsHover: boolean;
|
||||
desktopBp?: number;
|
||||
};
|
||||
|
||||
export function useNavToggleMenu({ desktopBp = 1024 }: Options) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [openSubmenuId, setOpenSubmenuId] = useState<string | null>(null);
|
||||
const [noTransition, setNoTransition] = useState(false);
|
||||
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
const clearRaf = useCallback(() => {
|
||||
if (rafRef.current != null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuOpen(false);
|
||||
setOpenSubmenuId(null);
|
||||
}, []);
|
||||
|
||||
const closeInstantNoAnim = useCallback(() => {
|
||||
clearRaf();
|
||||
setNoTransition(true);
|
||||
closeMenu();
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
setNoTransition(false);
|
||||
rafRef.current = null;
|
||||
});
|
||||
}, [clearRaf, closeMenu]);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
clearRaf();
|
||||
setNoTransition(false);
|
||||
|
||||
setMenuOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
setOpenSubmenuId(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [clearRaf]);
|
||||
|
||||
const toggleSubmenu = useCallback((id: string) => {
|
||||
setOpenSubmenuId((cur) => (cur === id ? null : id));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(`(min-width: ${desktopBp}px)`);
|
||||
|
||||
const onChange = () => {
|
||||
clearRaf();
|
||||
setNoTransition(true);
|
||||
closeMenu();
|
||||
requestAnimationFrame(() => setNoTransition(false));
|
||||
};
|
||||
|
||||
if (mql.addEventListener) {
|
||||
mql.addEventListener("change", onChange);
|
||||
} else {
|
||||
mql.addListener(onChange);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (mql.removeEventListener) {
|
||||
mql.removeEventListener("change", onChange);
|
||||
} else {
|
||||
mql.removeListener(onChange);
|
||||
}
|
||||
};
|
||||
}, [desktopBp, clearRaf, closeMenu]);
|
||||
|
||||
useEffect(() => clearRaf, [clearRaf]);
|
||||
|
||||
return {
|
||||
menuOpen,
|
||||
openSubmenuId,
|
||||
noTransition,
|
||||
setMenuOpen,
|
||||
setOpenSubmenuId,
|
||||
toggleMenu,
|
||||
toggleSubmenu,
|
||||
closeMenu,
|
||||
closeInstantNoAnim,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useSupportsHover() {
|
||||
const [supportsHover, setSupportsHover] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(hover: hover) and (pointer: fine)");
|
||||
|
||||
const update = () => setSupportsHover(mql.matches);
|
||||
|
||||
update();
|
||||
|
||||
if (mql.addEventListener) {
|
||||
mql.addEventListener("change", update);
|
||||
return () => mql.removeEventListener("change", update);
|
||||
}
|
||||
|
||||
mql.addListener(update);
|
||||
return () => mql.removeListener(update);
|
||||
}, []);
|
||||
|
||||
return supportsHover;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
type Options = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function useToggleBodyOverflow(locked: boolean, options: Options = {}) {
|
||||
const { className = "has-menu-open" } = options;
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
root.classList.toggle(className, locked);
|
||||
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = locked ? "hidden" : "";
|
||||
|
||||
return () => {
|
||||
root.classList.remove(className);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
};
|
||||
}, [locked, className]);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
trailingSlash: false,
|
||||
poweredByHeader: false,
|
||||
allowedDevOrigins: [
|
||||
"192.168.178.59", // deine PC-IP
|
||||
],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"next": "16.2.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"vanilla-cookieconsent": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><image width="1024" height="1024" xlink:href="data:image/png;base64,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 truncated
|
||||
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,244 @@
|
||||
:root {
|
||||
/* Colors */
|
||||
--btn-bg-primary: #474747;
|
||||
--btn-bg-primary-hover: #3b3b3b;
|
||||
--btn-bg-primary-active: #353030;
|
||||
|
||||
--btn-text-light: #fefcfb;
|
||||
--btn-text-dark: #474747;
|
||||
|
||||
--btn-border-primary: #474747;
|
||||
--btn-border-ghost: #e9e9e9;
|
||||
--btn-border-ghost-hover: #ececec;
|
||||
--btn-border-ghost-active: #e1e1e1;
|
||||
|
||||
--btn-bg-ghost-hover: #ececec;
|
||||
--btn-bg-ghost-active: #e1e1e1;
|
||||
|
||||
/* Disabled */
|
||||
--btn-bg-disabled: #cccccc;
|
||||
--btn-text-disabled: #888888;
|
||||
--btn-border-disabled: #cccccc;
|
||||
|
||||
/* Focus */
|
||||
--btn-focus-ring: 3px solid #a9a9a9;
|
||||
--btn-focus-offset: 2px;
|
||||
|
||||
/* Layout */
|
||||
--btn-py: 12px;
|
||||
--btn-px: 16px;
|
||||
--btn-py-lg: 16px;
|
||||
--btn-px-lg: 24px;
|
||||
--btn-radius: 4px;
|
||||
--btn-gap: 8px;
|
||||
--btn-max-height: 52px;
|
||||
--btn-icon-size: 18px;
|
||||
--btn-tertiary-px: 4px;
|
||||
|
||||
/* Typography */
|
||||
--btn-font-family: inherit;
|
||||
--btn-font-size: 0.889rem;
|
||||
--btn-font-weight: 600;
|
||||
--btn-line-height: 1;
|
||||
|
||||
/* Shadow */
|
||||
--btn-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--btn-shadow-none: none;
|
||||
|
||||
/* Motion */
|
||||
--btn-transition:
|
||||
background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease,
|
||||
transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--btn-gap);
|
||||
padding: var(--btn-py) var(--btn-px);
|
||||
max-height: var(--btn-max-height);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--btn-radius);
|
||||
font-family: var(--btn-font-family);
|
||||
font-size: var(--btn-font-size);
|
||||
font-weight: var(--btn-font-weight);
|
||||
line-height: var(--btn-line-height);
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: var(--btn-transition);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: var(--btn-focus-ring);
|
||||
outline-offset: var(--btn-focus-offset);
|
||||
}
|
||||
|
||||
.btn .btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: var(--btn-icon-size);
|
||||
height: var(--btn-icon-size);
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
color 0.25s ease;
|
||||
}
|
||||
|
||||
.btn:hover .btn-icon.animate {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Filled */
|
||||
.btn--filled {
|
||||
background-color: var(--btn-bg-primary);
|
||||
color: var(--btn-text-light);
|
||||
border-color: var(--btn-border-primary);
|
||||
box-shadow: var(--btn-shadow);
|
||||
}
|
||||
|
||||
.btn--filled .btn-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.btn--filled:hover {
|
||||
background-color: var(--btn-bg-primary-hover);
|
||||
border-color: var(--btn-bg-primary-hover);
|
||||
}
|
||||
|
||||
.btn--filled:active {
|
||||
background-color: var(--btn-bg-primary-active);
|
||||
border-color: var(--btn-bg-primary-active);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Outline */
|
||||
.btn--outline {
|
||||
background-color: transparent;
|
||||
color: var(--btn-text-dark);
|
||||
border-color: var(--btn-border-primary);
|
||||
box-shadow: var(--btn-shadow-none);
|
||||
}
|
||||
|
||||
.btn--outline .btn-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.btn--outline:hover {
|
||||
background-color: var(--btn-bg-primary);
|
||||
border-color: var(--btn-bg-primary);
|
||||
color: var(--btn-text-light);
|
||||
}
|
||||
|
||||
.btn--outline:active {
|
||||
background-color: var(--btn-bg-primary-active);
|
||||
border-color: var(--btn-bg-primary-active);
|
||||
color: var(--btn-text-light);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Ghost */
|
||||
.btn--ghost {
|
||||
background-color: transparent;
|
||||
color: var(--btn-text-dark);
|
||||
border-color: var(--btn-border-ghost);
|
||||
box-shadow: var(--btn-shadow-none);
|
||||
}
|
||||
|
||||
.btn--ghost .btn-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.btn--ghost:hover {
|
||||
background-color: var(--btn-bg-ghost-hover);
|
||||
border-color: var(--btn-border-ghost-hover);
|
||||
}
|
||||
|
||||
.btn--ghost:active {
|
||||
background-color: var(--btn-bg-ghost-active);
|
||||
border-color: var(--btn-border-ghost-active);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Tertiary */
|
||||
.btn--tertiary {
|
||||
background-color: transparent;
|
||||
color: var(--btn-text-dark);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--btn-shadow-none);
|
||||
padding-inline: var(--btn-tertiary-px);
|
||||
}
|
||||
|
||||
.btn--tertiary .btn-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.btn--tertiary:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.btn--tertiary:active {
|
||||
opacity: 0.7;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"] {
|
||||
background-color: var(--btn-bg-disabled) !important;
|
||||
border-color: var(--btn-border-disabled) !important;
|
||||
color: var(--btn-text-disabled) !important;
|
||||
box-shadow: none !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
pointer-events: none;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.btn:disabled .btn-icon,
|
||||
.btn[aria-disabled="true"] .btn-icon {
|
||||
color: var(--btn-text-disabled) !important;
|
||||
}
|
||||
|
||||
/* Touch */
|
||||
@media (hover: none) {
|
||||
.btn--filled:hover {
|
||||
background-color: var(--btn-bg-primary);
|
||||
border-color: var(--btn-bg-primary);
|
||||
}
|
||||
|
||||
.btn--outline:hover {
|
||||
background-color: transparent;
|
||||
border-color: var(--btn-border-primary);
|
||||
color: var(--btn-text-dark);
|
||||
}
|
||||
|
||||
.btn--ghost:hover {
|
||||
background-color: transparent;
|
||||
border-color: var(--btn-border-ghost);
|
||||
}
|
||||
|
||||
.btn--tertiary:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover .btn-icon.animate {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.btn {
|
||||
padding: var(--btn-py-lg) var(--btn-px-lg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
:root {
|
||||
/* Typography */
|
||||
--form-font-size: inherit;
|
||||
--form-font-family: inherit;
|
||||
--form-label-size: 0.875rem;
|
||||
--form-helper-size: 0.8rem;
|
||||
|
||||
/* Colors */
|
||||
--form-text: #171717;
|
||||
--form-placeholder: #9ca3af;
|
||||
--form-label: #374151;
|
||||
--form-border: #d1d5db;
|
||||
--form-border-hover: #bfc5cf;
|
||||
--form-border-focus: #474747;
|
||||
--form-border-error: #ef4444;
|
||||
--form-bg: #ffffff;
|
||||
--form-bg-disabled: #f3f4f6;
|
||||
--form-text-disabled: #9ca3af;
|
||||
--form-error: #ef4444;
|
||||
--form-success: #22c55e;
|
||||
--form-check-accent: #474747;
|
||||
|
||||
/* Layout */
|
||||
--form-radius: 8px;
|
||||
--form-padding-y: 10px;
|
||||
--form-padding-x: 14px;
|
||||
--form-gap: 6px;
|
||||
--form-field-gap: 1.25rem;
|
||||
--form-control-gap: 10px;
|
||||
--form-min-height: 44px;
|
||||
|
||||
/* Motion */
|
||||
--form-transition:
|
||||
border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
/* Focus */
|
||||
--form-focus-shadow: 0 0 0 3px rgba(71, 71, 71, 0.15);
|
||||
--form-error-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* Group */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--form-gap);
|
||||
margin-bottom: var(--form-field-gap);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
label,
|
||||
.form-label {
|
||||
font-size: var(--form-label-size);
|
||||
font-weight: 600;
|
||||
color: var(--form-label);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-label--required::after {
|
||||
content: " *";
|
||||
color: var(--form-error);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="tel"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="search"],
|
||||
input[type="url"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-family: var(--form-font-family);
|
||||
font-size: var(--form-font-size);
|
||||
color: var(--form-text);
|
||||
background-color: var(--form-bg);
|
||||
border: 1.5px solid var(--form-border);
|
||||
border-radius: var(--form-radius);
|
||||
padding: var(--form-padding-y) var(--form-padding-x);
|
||||
min-height: var(--form-min-height);
|
||||
transition: var(--form-transition);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
input:hover,
|
||||
textarea:hover,
|
||||
select:hover {
|
||||
border-color: var(--form-border-hover);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
border-color: var(--form-border-focus);
|
||||
box-shadow: var(--form-focus-shadow);
|
||||
}
|
||||
|
||||
/* Error */
|
||||
input.is-error,
|
||||
textarea.is-error,
|
||||
select.is-error {
|
||||
border-color: var(--form-border-error);
|
||||
}
|
||||
|
||||
input.is-error:focus-visible,
|
||||
textarea.is-error:focus-visible,
|
||||
select.is-error:focus-visible {
|
||||
box-shadow: var(--form-error-shadow);
|
||||
}
|
||||
|
||||
.form-error-msg {
|
||||
font-size: var(--form-helper-size);
|
||||
color: var(--form-error);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Helper / Success */
|
||||
.form-help {
|
||||
font-size: var(--form-helper-size);
|
||||
color: var(--form-label);
|
||||
}
|
||||
|
||||
.form-success-msg {
|
||||
font-size: var(--form-helper-size);
|
||||
color: var(--form-success);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
input:disabled,
|
||||
textarea:disabled,
|
||||
select:disabled {
|
||||
background-color: var(--form-bg-disabled);
|
||||
color: var(--form-text-disabled);
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Select */
|
||||
select {
|
||||
cursor: pointer;
|
||||
padding-right: 40px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23474747' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Checkbox / Radio */
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--form-control-gap);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check input[type="checkbox"],
|
||||
.form-check input[type="radio"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
border: 1.5px solid var(--form-border);
|
||||
background-color: var(--form-bg);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: var(--form-transition);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.form-check input[type="checkbox"] {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-check input[type="radio"] {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.form-check input[type="checkbox"]:checked,
|
||||
.form-check input[type="radio"]:checked {
|
||||
background-color: var(--form-check-accent);
|
||||
border-color: var(--form-check-accent);
|
||||
}
|
||||
|
||||
.form-check input[type="checkbox"]:checked {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.form-check input[type="radio"]:checked {
|
||||
box-shadow: inset 0 0 0 4px white;
|
||||
}
|
||||
|
||||
.form-check input:focus-visible {
|
||||
box-shadow: var(--form-focus-shadow);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--form-text);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Touch */
|
||||
@media (hover: none) {
|
||||
input:hover,
|
||||
textarea:hover,
|
||||
select:hover {
|
||||
border-color: var(--form-border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
:root {
|
||||
/* Layout */
|
||||
--container-width: 1280px;
|
||||
--content-width: 72rem;
|
||||
--padding-inline: clamp(16px, 4vw, 24px);
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-7: 32px;
|
||||
|
||||
/* Colors */
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f9f9f9;
|
||||
--color-background: #ffffff;
|
||||
--color-background-alt: #f9f9f9;
|
||||
--color-text: #171717;
|
||||
--color-text-muted: #5f5f5f;
|
||||
--color-text-soft: #7a7a7a;
|
||||
--color-heading: #171717;
|
||||
|
||||
/* Radius */
|
||||
--radius-none: 0;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 24px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
:root {
|
||||
/* Font sizes */
|
||||
--font-size-body: 1rem;
|
||||
--font-size-small: 0.875rem;
|
||||
--font-size-lead: 1.125rem;
|
||||
--font-size-h1: clamp(
|
||||
2.566rem,
|
||||
calc(
|
||||
2.566rem + (3.247rem - 2.566rem) * ((100vw - 768px) / (1024px - 768px))
|
||||
),
|
||||
3.247rem
|
||||
);
|
||||
--font-size-h2: clamp(
|
||||
2.027rem,
|
||||
calc(
|
||||
2.027rem + (2.566rem - 2.027rem) * ((100vw - 768px) / (1024px - 768px))
|
||||
),
|
||||
2.566rem
|
||||
);
|
||||
|
||||
--font-size-h3: clamp(
|
||||
1.602rem,
|
||||
calc(
|
||||
1.602rem + (2.027rem - 1.602rem) * ((100vw - 768px) / (1024px - 768px))
|
||||
),
|
||||
2.027rem
|
||||
);
|
||||
|
||||
--font-size-h4: clamp(
|
||||
1.266rem,
|
||||
calc(
|
||||
1.266rem + (1.602rem - 1.266rem) * ((100vw - 768px) / (1024px - 768px))
|
||||
),
|
||||
1.602rem
|
||||
);
|
||||
|
||||
--line-height-body: 1.5;
|
||||
--line-height-heading: 1.2;
|
||||
|
||||
--letter-spacing-tight: -0.02em;
|
||||
--letter-spacing-normal: 0;
|
||||
--letter-spacing-wide: 0.025em;
|
||||
--letter-spacing-wider: 0.05em;
|
||||
--letter-spacing-widest: 0.12em;
|
||||
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--font-body-color: #000000;
|
||||
--font-heading-color: #302e2e;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
font-size: var(--font-size-body);
|
||||
line-height: var(--line-height-body);
|
||||
}
|
||||
|
||||
small,
|
||||
.text-small {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lead,
|
||||
.text-lead {
|
||||
font-size: var(--font-size-lead);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: var(--line-height-heading);
|
||||
color: var(--font-heading-color);
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--font-weight-bold);
|
||||
letter-spacing: var(--letter-spacing-tight);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-h1);
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--font-size-h3);
|
||||
}
|
||||
h4 {
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||