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,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAABgmlDQ1BzUkdCIElFQzYxOTY2LTIuMQAAKJF1kc8rw2Ecx182opkoDg7SEk6mmVpcHCZG4bBNGS7bd7/Ufnz7fre0XJXrihIXvw78BVyVs1JESs5yJC7o6/PdVluyz9Pn+bye9/N8Pj3P5wFLMKWk9UYXpDM5ze/zOpZCy47mVxrpwUYvnrCiq/OB6SB17fOeBjPeOs1a9c/9a63RmK5AQ4vwhKJqOeEZ4bn1nGryjnCXkgxHhc+EhzS5oPCdqUfK/GJyoszfJmtB/yRYOoQdiRqO1LCS1NLC8nL606m8UrmP+RJ7LLMYkNgn3oOOHx9eHMwyxSQeRhiX2YMTN8Oyok6+q5S/QFZyFZlVCmiskSBJjiFR81I9JjEuekxGioLZ/7991eOj7nJ1uxeang3jfQCat+GnaBhfR4bxcwzWJ7jMVPOzhzD2IXqxqvUfQPsmnF9VtcguXGxB96Ma1sIlySpuicfh7RTaQtB5A7aVcs8q+5w8QHBDvuoa9vZhUM63r/4CshFoCPT1KYIAAAAJcEhZcwAACxMAAAsTAQCanBgAACAASURBVHic7N15uCVnXS/6bxIICXQYEgZBaMCGBpVBQBApBKGtZioG7xFQhKBynJBBURACDvdeQYigMggKHqYgo4piIaGLJgpUuBwQJYCQ9tAJNhAkCWDSZOh0p+8fa+2we97DWutdw+fzPPWs3bv3qvom1V39q99637cSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFbuuNIBGK2qbo5PcsskNx9upx309SlJTkxyg4NeT4w/DwCL7toke4bb1Qd9fVmSS5JcOnxd+vriJJf0Xbu/RGCARVHVzXFJbpHkJhnU9IfbNhzhe3uSXL7K7eK+a6+azH8dk+KGbwZVdXNykh9Icuckd0hyx2Wvt8/gZh4AJuXKJBcmueCg1/OTnN937Z5SwQBmTVU3N0qyOcldlm13HX7vRhOMsj/LruXLti8muUjjdzZpAEyxYZdvY5J7JrnHsu3OSY4vGA0AVuqaDIrF85Ztn+m79qKiqQAKGtb5t0vy/TnwRv8uSW5bMNpK7c6hjYHzk3yh79qrSwbj6DQApkhVN9dP8kNJHpikGm7fUzQUAIzHhUn6Zdvn+67dVzQRwBhVdfO9SR66bNtYNtFYXJnkY0k+PNw+3Xft3rKRWE4DoKDhfP17J3l4ki1J7pfkhkVDAUAZlyU5N8mHknwgg0+RDC8FZlZVN7dI8uP57g3/5qKByrgsyUfy3YbAZ/uuvbZspMWmATBhVd2clmRrkkckeVgGC/YBAAf6zyRnZ9AM2N537eWF8wAcVVU3N03yoAxu9h+SwdRdDnRpknPy3YbADs3eydIAmICqbm6c5KeSnJ7BRcH/dwBYub1JtiV5a5L39V17ZeE8AEkOqfN/LNbpWq2vJHlHkrf2Xfu50mEWgRvRManq5npJ6gwuBo9LclLZRAAwFy5L8u4MmgG9oaTApKnzx+ZfM7i2v6Pv2v8qHWZeaQCM0HA1z3tmcDF4UpJblU0EAHPtgiRvS3JW37X/UToMML/U+RO1L4MpYGfFqK+R0wAYgapubpPBheD0JHcvHAcAFtHHM/jk6N19136zdBhgPqjzizPqa8Q0ANZouIL/Y5L8SgZDgMz3AYDy9iRpk7wmyT9ZXApYLXX+1LowyZuT/FnftZeUjTK7NABWaTjn5wlJzkjyg4XjAABHdm6SFyf5gEYAcCzq/JlxRZI/T/KKvmu/VjrMrNEAWKGqbk5M8pQkL0iyqXAcAGDl/jWDRsB7DR8FDraszn9+kjsVjsPK7Unyv5Kc2XfthYWzzAwNgGOo6ubkJE9L8rwktyscBwBYuy8keUmSd/Zdu7d0GKAsdf7c2JvBgrAv7bv2/NJhpp0GwBFUdXNKBvN+fjNW+QSAebIzycuSvKXv2qtLhwEmS50/t/YneU+SF/dde17pMNNKA+AgVd3cLMmzkjw7yc0KxwEAxuerSf4oyRv6rr2idBhgvIZ1/jOT/HrU+fPuHzJoBHyidJBpowEwVNXNCUl+OYM5gjctHAcAmJyvJfmNJO+xWCDMn2Gd/0sZTAFS5y+W9yR5Tt+1XykdZFpoACSp6ua+SV6X5D6lswAAxXRJntF37Y7SQYDRGNb5r03yw6WzUMx3kvxeklf1XXtN6TClLXQDYDgM6CUZfPK/0P8vAIAkg1Wlz0zykr5rrywdBlibYZ3/4gzm+qvzSZLPJXl637UfLR2kpIX8y1DVzfFJTs/gH/hbFI4DAEyfC5I8q+/atnQQYOWqujkugzr/j6LO5/DekuR5fdd+o3SQEhauAVDVzd0zGAb0wNJZAICp9/dJnt137ZdLBwGOTp3PKnw7yRlJXt937b7SYSZpYRoAw8d9/H4Gq/ufUDYNADBDrkzy/yZ5Rd+1e0qHAQ6kzmcdPpXBtIBPlg4yKQvRAKjq5rEZdANvUzoLADCzzk/ytL5r+9JBgIGqbh6TwWLe6nzWan+SP0/y3L5rv1M6zLjNdQOgqpsbZDDP/1mlswAAc2FfkhclObPv2mtLh4FFVdXNiRnU+c8unYW58YUkj++79vOlg4zT3DYAqrr5viTvjkf7AQCjd3aS0/uuvbh0EFg0wzr/XfFoP0bvyiS/luTNfdfuLx1mHI4vHWAcqrr5H0n+NW7+AYDxeHiSf6vq5kGlg8AiWVbnu/lnHE5O8sYkb6nqZkPpMOMwVyMAhkP+X57kGaWzAAAL4dokv5vkD00JgPFR51PAF5M8oe/az5YOMkpz0wCo6mZTBkP+7106CwCwcLYlecqiPlcaxmlY578rRvcyeVdl0HR647xMCZiLKQBV3Tw+yafj5h8AKGNrBlMCHlw6CMyTZXW+m39KOCnJXyY5a16mBMz0CICqbk5K8ookTy+dBQAggykBv5fBlIB9pcPArFLnM4XOz2BKwHmlg6zHzDYAqro5LUmb5P6lswAAHOR9SX6679orSweBWaPOZ4pdleSJfde+r3SQtZrJBkBVNxuTfDDJXUtnAQA4go8leUzftd8qHQRmhTqfGXBtkl/su/aNpYOsxcw1AKq6+cEMnr1729JZAACO4XNJHtZ37ddKB4Fpp85nxpyR5KWztjjgCaUDrEZVNw9I8qEktyqdBQBgBW6Z5Kc2btr8gV07d1xaOgxMK3U+M2hLkptu3LR5266dO2amCTAzTwGo6uZRGVwUblY6CwDAKtw+ycequrlv6SAwjdT5zLBnJ3lbVTcnlg6yUjPRAKjq5qlJ/j7JyaWzAACswc2TnFPVzdbSQWCaqPOZAz+T5B9m5TGBU70GQFU3xyV5bpKXlc4CADACe5M8te/at5cOAqVVdfPcJGeWzgEj8skkj+q79uLSQY5mahsAVd0cn+SPkjyndBYAgBH79b5rX1k6BJQwrPPPTPKbpbPAiO3IYOHXC0sHOZKpXARweFF4U5JfKZ0FAGAMHr5x0+YTdu3ccU7pIDBJwzr/jUl+tXQWLine 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"]
|
||||||
|
}
|
||||||