feat(shared-landing-ui): unify landing pages with shared components

Add new reusable components to shared-landing-ui package:
- AppScrollerSection, TimelineSection, MasonryGridSection, PrinciplesSection
- LegalPageTemplate for privacy/terms/cookies/imprint pages
- Navigation component with mobile menu and language switcher
- GradientText and LanguageSwitcher atoms
- i18n system with getLangFromUrl, useTranslations, localizePath
- Theme files for picture (indigo), chat (blue), zitare (teal)

Add legal pages to ManaDeck and Chat landing pages:
- privacy, terms, cookies, imprint pages using shared template
- Updated footers with cookies link
This commit is contained in:
Till-JS 2026-01-23 15:45:47 +01:00
parent 6d86a08d63
commit 264149a913
25 changed files with 3589 additions and 1 deletions

View file

@ -8,6 +8,7 @@ const footerLinks = {
legal: [
{ href: '/privacy', label: 'Datenschutz' },
{ href: '/terms', label: 'AGB' },
{ href: '/cookies', label: 'Cookies' },
{ href: '/imprint', label: 'Impressum' },
],
};

View file

@ -0,0 +1,67 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Cookie-Richtlinie - ManaChat">
<LegalPageTemplate
title="Cookie-Richtlinie"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Was sind Cookies?</h2>
<p>
Cookies sind kleine Textdateien, die auf Ihrem Gerät gespeichert werden. Sie helfen uns, die
Funktionalität unserer Website zu verbessern und Ihre Nutzererfahrung zu personalisieren.
</p>
<h2>2. Welche Cookies verwenden wir?</h2>
<h3>Notwendige Cookies</h3>
<p>Diese Cookies sind für den Betrieb der Website unerlässlich:</p>
<ul>
<li><strong>Session-Cookie:</strong> Hält Sie eingeloggt</li>
<li><strong>Sprach-Cookie:</strong> Speichert Ihre Spracheinstellung</li>
<li><strong>Auth-Token:</strong> Sichert Ihre Anmeldung</li>
</ul>
<h3>Funktionale Cookies</h3>
<p>Diese Cookies verbessern Ihre Nutzererfahrung:</p>
<ul>
<li><strong>Chat-Einstellungen:</strong> Speichern Ihre bevorzugten KI-Modelle</li>
<li><strong>UI-Präferenzen:</strong> Dark Mode, Schriftgröße etc.</li>
</ul>
<h3>Analyse-Cookies</h3>
<p>Wir verwenden Umami Analytics, eine datenschutzfreundliche Alternative:</p>
<ul>
<li>Anonymisierte Nutzungsstatistiken</li>
<li>Keine personenbezogenen Daten</li>
<li>Keine Tracking über Websites hinweg</li>
</ul>
<h2>3. Cookies von Drittanbietern</h2>
<p>
Wir verwenden keine Tracking-Cookies von Drittanbietern wie Google Analytics oder Facebook.
</p>
<h2>4. Ihre Cookie-Einstellungen</h2>
<p>Sie können Cookies in Ihren Browser-Einstellungen verwalten:</p>
<ul>
<li>Alle Cookies blockieren</li>
<li>Cookies beim Schließen des Browsers löschen</li>
<li>Drittanbieter-Cookies separat verwalten</li>
</ul>
<h2>5. Auswirkungen der Cookie-Deaktivierung</h2>
<p>
Wenn Sie notwendige Cookies deaktivieren, funktionieren einige Funktionen der App
möglicherweise nicht mehr korrekt, z.B. das automatische Einloggen oder die Speicherung Ihrer
Chat-Einstellungen.
</p>
<h2>6. Kontakt</h2>
<p>Bei Fragen zu unserer Cookie-Richtlinie kontaktieren Sie uns unter: privacy@mana.how</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,90 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Impressum - ManaChat">
<LegalPageTemplate
title="Impressum"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>Angaben gemäß § 5 TMG</h2>
<p>
Mana Technologies<br />
Musterstraße 1<br />
12345 Musterstadt<br />
Deutschland
</p>
<h2>Kontakt</h2>
<p>
E-Mail: contact@mana.how<br />
Telefon: +49 (0) 123 456789
</p>
<h2>Vertreten durch</h2>
<p>Geschäftsführer: Max Mustermann</p>
<h2>Handelsregister</h2>
<p>
Registergericht: Amtsgericht Musterstadt<br />
Registernummer: HRB 12345
</p>
<h2>Umsatzsteuer-ID</h2>
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:<br />
DE123456789
</p>
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Max Mustermann<br />
Musterstraße 1<br />
12345 Musterstadt
</p>
<h2>Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr" target="_blank" rel="noopener"
>https://ec.europa.eu/consumers/odr</a
>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
<h2>Haftungsausschluss</h2>
<h3>Haftung für Inhalte</h3>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch
nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen.
</p>
<h3>Haftung für KI-generierte Inhalte</h3>
<p>
Die von der KI generierten Antworten sind keine rechtsverbindlichen Aussagen. Die Nutzung
erfolgt auf eigene Verantwortung.
</p>
<h3>Haftung für Links</h3>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen
Einfluss haben. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter
verantwortlich.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,72 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Datenschutzerklärung - ManaChat">
<LegalPageTemplate
title="Datenschutzerklärung"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Einleitung</h2>
<p>
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der
Verarbeitung personenbezogener Daten innerhalb unserer ManaChat-Anwendung.
</p>
<h2>2. Verantwortlicher</h2>
<p>
Verantwortlich für die Datenverarbeitung ist:<br />
Mana Technologies<br />
Musterstraße 1<br />
12345 Musterstadt<br />
Deutschland<br />
E-Mail: privacy@mana.how
</p>
<h2>3. Erhobene Daten</h2>
<p>Wir erheben folgende Daten:</p>
<ul>
<li>E-Mail-Adresse bei Registrierung</li>
<li>Chat-Verläufe und Konversationen</li>
<li>Nutzungsstatistiken zur Verbesserung der App</li>
<li>Geräteinformationen für die Synchronisierung</li>
</ul>
<h2>4. Zweck der Datenverarbeitung</h2>
<p>Ihre Daten werden verwendet für:</p>
<ul>
<li>Bereitstellung des KI-Chat-Dienstes</li>
<li>Speicherung Ihrer Konversationen</li>
<li>Synchronisierung über Geräte hinweg</li>
<li>Kommunikation bezüglich Ihres Kontos</li>
</ul>
<h2>5. KI-Datenverarbeitung</h2>
<p>
Ihre Chat-Nachrichten werden an KI-Dienste (OpenRouter) zur Verarbeitung übermittelt. Diese
Dienste nutzen Ihre Daten nicht für das Training ihrer Modelle.
</p>
<h2>6. Datenspeicherung</h2>
<p>
Ihre Daten werden auf sicheren Servern in der Europäischen Union gespeichert. Wir verwenden
Verschlüsselung für alle übertragenen und gespeicherten Daten.
</p>
<h2>7. Ihre Rechte</h2>
<p>Sie haben folgende Rechte:</p>
<ul>
<li>Auskunft über Ihre gespeicherten Daten</li>
<li>Berichtigung unrichtiger Daten</li>
<li>Löschung Ihrer Daten und Chat-Verläufe</li>
<li>Export Ihrer Konversationen</li>
<li>Widerruf der Einwilligung</li>
</ul>
<h2>8. Kontakt</h2>
<p>Bei Fragen zum Datenschutz kontaktieren Sie uns unter: privacy@mana.how</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,84 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Nutzungsbedingungen - ManaChat">
<LegalPageTemplate
title="Nutzungsbedingungen"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Geltungsbereich</h2>
<p>
Diese Nutzungsbedingungen gelten für die Nutzung der ManaChat-Anwendung, einschließlich der
mobilen Apps, der Web-Version und aller zugehörigen Dienste.
</p>
<h2>2. Leistungsbeschreibung</h2>
<p>ManaChat bietet:</p>
<ul>
<li>Zugang zu verschiedenen KI-Modellen (GPT-4o, GPT-4o-Mini, etc.)</li>
<li>Speicherung von Chat-Verläufen</li>
<li>Synchronisierung über mehrere Geräte</li>
<li>Dokument-Modus für längere Texte</li>
</ul>
<h2>3. Registrierung</h2>
<p>
Zur Nutzung ist eine Registrierung erforderlich. Sie sind für die Geheimhaltung Ihrer
Zugangsdaten verantwortlich.
</p>
<h2>4. Kostenfreie und kostenpflichtige Funktionen</h2>
<p>ManaChat bietet sowohl kostenfreie als auch Premium-Funktionen:</p>
<ul>
<li><strong>Free:</strong> Begrenzte Nachrichten, GPT-4o-Mini</li>
<li><strong>Pro:</strong> Unbegrenzte Nachrichten, alle KI-Modelle</li>
<li><strong>Team:</strong> Zusätzliche Teamfunktionen und API-Zugang</li>
</ul>
<h2>5. Nutzerverhalten</h2>
<p>Sie verpflichten sich:</p>
<ul>
<li>Die KI nicht für illegale oder schädliche Zwecke zu nutzen</li>
<li>Keine automatisierten Massenanfragen zu senden</li>
<li>Keine versuche zu unternehmen, das System zu manipulieren</li>
<li>Die Nutzungsbedingungen der KI-Modelle zu respektieren</li>
</ul>
<h2>6. KI-generierte Inhalte</h2>
<p>
KI-generierte Antworten können fehlerhaft sein. Sie sind für die Überprüfung und Nutzung der
generierten Inhalte selbst verantwortlich.
</p>
<h2>7. Verfügbarkeit</h2>
<p>
Wir bemühen uns um eine hohe Verfügbarkeit. Die Verfügbarkeit kann jedoch von Drittanbietern
(KI-Dienste) abhängen und ist nicht garantiert.
</p>
<h2>8. Kündigung</h2>
<p>
Sie können Ihr Konto jederzeit kündigen. Bei Premium-Abonnements gilt die Kündigung zum Ende
des Abrechnungszeitraums.
</p>
<h2>9. Haftung</h2>
<p>
Die Haftung für KI-generierte Inhalte ist ausgeschlossen. Wir haften nur für Vorsatz und grobe
Fahrlässigkeit.
</p>
<h2>10. Änderungen</h2>
<p>
Wir behalten uns vor, diese Nutzungsbedingungen zu ändern. Wesentliche Änderungen werden Ihnen
rechtzeitig mitgeteilt.
</p>
<h2>11. Anwendbares Recht</h2>
<p>Es gilt deutsches Recht. Gerichtsstand ist, soweit zulässig, der Sitz des Anbieters.</p>
</LegalPageTemplate>
</Layout>

View file

@ -8,6 +8,7 @@ const footerLinks = {
legal: [
{ href: '/privacy', label: 'Datenschutz' },
{ href: '/terms', label: 'AGB' },
{ href: '/cookies', label: 'Cookies' },
{ href: '/imprint', label: 'Impressum' },
],
};

View file

@ -0,0 +1,67 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Cookie-Richtlinie - ManaDeck">
<LegalPageTemplate
title="Cookie-Richtlinie"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Was sind Cookies?</h2>
<p>
Cookies sind kleine Textdateien, die auf Ihrem Gerät gespeichert werden. Sie helfen uns, die
Funktionalität unserer Website zu verbessern und Ihre Nutzererfahrung zu personalisieren.
</p>
<h2>2. Welche Cookies verwenden wir?</h2>
<h3>Notwendige Cookies</h3>
<p>Diese Cookies sind für den Betrieb der Website unerlässlich:</p>
<ul>
<li><strong>Session-Cookie:</strong> Hält Sie eingeloggt</li>
<li><strong>Sprach-Cookie:</strong> Speichert Ihre Spracheinstellung</li>
</ul>
<h3>Funktionale Cookies</h3>
<p>Diese Cookies verbessern Ihre Nutzererfahrung:</p>
<ul>
<li><strong>Präferenz-Cookies:</strong> Speichern Ihre Einstellungen</li>
<li><strong>Lernfortschritts-Cookies:</strong> Ermöglichen Offline-Lernen</li>
</ul>
<h3>Analyse-Cookies</h3>
<p>
Wir verwenden Umami Analytics, eine datenschutzfreundliche Alternative zu Google Analytics:
</p>
<ul>
<li>Anonymisierte Nutzungsstatistiken</li>
<li>Keine personenbezogenen Daten</li>
<li>Keine Tracking über Websites hinweg</li>
</ul>
<h2>3. Cookies von Drittanbietern</h2>
<p>
Wir verwenden keine Tracking-Cookies von Drittanbietern wie Google Analytics oder Facebook.
</p>
<h2>4. Ihre Cookie-Einstellungen</h2>
<p>Sie können Cookies in Ihren Browser-Einstellungen verwalten:</p>
<ul>
<li>Alle Cookies blockieren</li>
<li>Cookies beim Schließen des Browsers löschen</li>
<li>Drittanbieter-Cookies separat verwalten</li>
</ul>
<h2>5. Auswirkungen der Cookie-Deaktivierung</h2>
<p>
Wenn Sie notwendige Cookies deaktivieren, funktionieren einige Funktionen der App
möglicherweise nicht mehr korrekt, z.B. das automatische Einloggen.
</p>
<h2>6. Kontakt</h2>
<p>Bei Fragen zu unserer Cookie-Richtlinie kontaktieren Sie uns unter: privacy@mana.how</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,84 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Impressum - ManaDeck">
<LegalPageTemplate
title="Impressum"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>Angaben gemäß § 5 TMG</h2>
<p>
Mana Technologies<br />
Musterstraße 1<br />
12345 Musterstadt<br />
Deutschland
</p>
<h2>Kontakt</h2>
<p>
E-Mail: contact@mana.how<br />
Telefon: +49 (0) 123 456789
</p>
<h2>Vertreten durch</h2>
<p>Geschäftsführer: Max Mustermann</p>
<h2>Handelsregister</h2>
<p>
Registergericht: Amtsgericht Musterstadt<br />
Registernummer: HRB 12345
</p>
<h2>Umsatzsteuer-ID</h2>
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:<br />
DE123456789
</p>
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Max Mustermann<br />
Musterstraße 1<br />
12345 Musterstadt
</p>
<h2>Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr" target="_blank" rel="noopener"
>https://ec.europa.eu/consumers/odr</a
>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
<h2>Haftungsausschluss</h2>
<h3>Haftung für Inhalte</h3>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch
nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen.
</p>
<h3>Haftung für Links</h3>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen
Einfluss haben. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter
verantwortlich.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,72 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Datenschutzerklärung - ManaDeck">
<LegalPageTemplate
title="Datenschutzerklärung"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Einleitung</h2>
<p>
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der
Verarbeitung personenbezogener Daten innerhalb unserer ManaDeck-Anwendung.
</p>
<h2>2. Verantwortlicher</h2>
<p>
Verantwortlich für die Datenverarbeitung ist:<br />
Mana Technologies<br />
Musterstraße 1<br />
12345 Musterstadt<br />
Deutschland<br />
E-Mail: privacy@mana.how
</p>
<h2>3. Erhobene Daten</h2>
<p>Wir erheben folgende Daten:</p>
<ul>
<li>E-Mail-Adresse bei Registrierung</li>
<li>Karteikarten und Lernfortschritte</li>
<li>Nutzungsstatistiken zur Verbesserung der App</li>
<li>Geräteinformationen für die Synchronisierung</li>
</ul>
<h2>4. Zweck der Datenverarbeitung</h2>
<p>Ihre Daten werden verwendet für:</p>
<ul>
<li>Bereitstellung des Karteikarten-Dienstes</li>
<li>Synchronisierung Ihrer Lernfortschritte</li>
<li>Verbesserung der KI-generierten Karteikarten</li>
<li>Kommunikation bezüglich Ihres Kontos</li>
</ul>
<h2>5. Datenspeicherung</h2>
<p>
Ihre Daten werden auf sicheren Servern in der Europäischen Union gespeichert. Wir verwenden
Verschlüsselung für alle übertragenen und gespeicherten Daten.
</p>
<h2>6. Ihre Rechte</h2>
<p>Sie haben folgende Rechte:</p>
<ul>
<li>Auskunft über Ihre gespeicherten Daten</li>
<li>Berichtigung unrichtiger Daten</li>
<li>Löschung Ihrer Daten</li>
<li>Export Ihrer Daten</li>
<li>Widerruf der Einwilligung</li>
</ul>
<h2>7. KI-Datenverarbeitung</h2>
<p>
Bei der KI-gestützten Karteikarten-Generierung werden Ihre Texte an KI-Dienste übermittelt.
Diese Daten werden nur zur Verarbeitung verwendet und nicht dauerhaft gespeichert.
</p>
<h2>8. Kontakt</h2>
<p>Bei Fragen zum Datenschutz kontaktieren Sie uns unter: privacy@mana.how</p>
</LegalPageTemplate>
</Layout>

View file

@ -0,0 +1,83 @@
---
import Layout from '../layouts/Layout.astro';
import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
---
<Layout title="Nutzungsbedingungen - ManaDeck">
<LegalPageTemplate
title="Nutzungsbedingungen"
backLink="/"
backText="Zurück zur Startseite"
lastUpdatedText="Zuletzt aktualisiert"
>
<h2>1. Geltungsbereich</h2>
<p>
Diese Nutzungsbedingungen gelten für die Nutzung der ManaDeck-Anwendung, einschließlich der
mobilen Apps und der Web-Version.
</p>
<h2>2. Leistungsbeschreibung</h2>
<p>ManaDeck bietet:</p>
<ul>
<li>KI-gestützte Erstellung von Karteikarten</li>
<li>Spaced-Repetition-Lernsystem</li>
<li>Synchronisierung über mehrere Geräte</li>
<li>Import von Texten und Dokumenten</li>
</ul>
<h2>3. Registrierung</h2>
<p>
Zur Nutzung ist eine Registrierung erforderlich. Sie sind für die Geheimhaltung Ihrer
Zugangsdaten verantwortlich.
</p>
<h2>4. Kostenfreie und kostenpflichtige Funktionen</h2>
<p>ManaDeck bietet sowohl kostenfreie als auch Premium-Funktionen:</p>
<ul>
<li><strong>Free:</strong> Grundfunktionen mit begrenzten Karten</li>
<li><strong>Pro:</strong> Unbegrenzte Karten und erweiterte KI-Funktionen</li>
<li><strong>Team:</strong> Zusätzliche Teamfunktionen</li>
</ul>
<h2>5. Nutzerverhalten</h2>
<p>Sie verpflichten sich:</p>
<ul>
<li>Keine urheberrechtlich geschützten Inhalte ohne Berechtigung hochzuladen</li>
<li>Den Dienst nicht missbräuchlich zu nutzen</li>
<li>Keine automatisierten Zugriffe ohne Genehmigung durchzuführen</li>
</ul>
<h2>6. Geistiges Eigentum</h2>
<p>
Die von Ihnen erstellten oder generierten Karteikarten gehören Ihnen. ManaDeck behält die
Rechte an der Software und dem Design.
</p>
<h2>7. Verfügbarkeit</h2>
<p>
Wir bemühen uns um eine hohe Verfügbarkeit, können jedoch keine 100%ige Verfügbarkeit
garantieren. Wartungsarbeiten werden nach Möglichkeit vorab angekündigt.
</p>
<h2>8. Kündigung</h2>
<p>
Sie können Ihr Konto jederzeit kündigen. Bei Premium-Abonnements gilt die Kündigung zum Ende
des Abrechnungszeitraums.
</p>
<h2>9. Haftung</h2>
<p>
Die Haftung ist auf Vorsatz und grobe Fahrlässigkeit beschränkt. Dies gilt nicht für Schäden
aus der Verletzung von Leben, Körper oder Gesundheit.
</p>
<h2>10. Änderungen</h2>
<p>
Wir behalten uns vor, diese Nutzungsbedingungen zu ändern. Wesentliche Änderungen werden Ihnen
rechtzeitig mitgeteilt.
</p>
<h2>11. Anwendbares Recht</h2>
<p>Es gilt deutsches Recht. Gerichtsstand ist, soweit zulässig, der Sitz des Anbieters.</p>
</LegalPageTemplate>
</Layout>

View file

@ -9,12 +9,17 @@
"./atoms/*": "./src/atoms/*",
"./sections/*": "./src/sections/*",
"./layouts/*": "./src/layouts/*",
"./templates/*": "./src/templates/*",
"./utils": "./src/utils/index.ts",
"./i18n": "./src/i18n/index.ts",
"./themes": "./src/themes/index.css",
"./themes/memoro": "./src/themes/memoro.css",
"./themes/manacore": "./src/themes/manacore.css",
"./themes/maerchenzauber": "./src/themes/maerchenzauber.css",
"./themes/manadeck": "./src/themes/manadeck.css"
"./themes/manadeck": "./src/themes/manadeck.css",
"./themes/picture": "./src/themes/picture.css",
"./themes/chat": "./src/themes/chat.css",
"./themes/zitare": "./src/themes/zitare.css"
},
"files": [
"src"

View file

@ -0,0 +1,82 @@
---
/**
* GradientText - Text with gradient color effect
*
* Usage:
* ```astro
* <GradientText>Highlighted Text</GradientText>
* <GradientText as="span" gradient="secondary">Custom Gradient</GradientText>
* ```
*/
interface Props {
as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div';
gradient?: 'primary' | 'secondary' | 'accent' | 'rainbow';
class?: string;
}
const { as: Element = 'span', gradient = 'primary', class: className = '' } = Astro.props;
---
<Element class:list={['gradient-text', `gradient-${gradient}`, className]}>
<slot />
</Element>
<style>
.gradient-text {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
.gradient-primary {
background: linear-gradient(
135deg,
var(--color-primary) 0%,
var(--color-primary-hover) 50%,
var(--color-primary) 100%
);
}
.gradient-secondary {
background: linear-gradient(
135deg,
var(--color-text-primary) 0%,
var(--color-primary) 50%,
var(--color-text-primary) 100%
);
}
.gradient-accent {
background: linear-gradient(135deg, #f97316 0%, #ec4899 50%, #8b5cf6 100%);
}
.gradient-rainbow {
background: linear-gradient(
135deg,
#ef4444 0%,
#f97316 15%,
#eab308 30%,
#22c55e 45%,
#3b82f6 60%,
#8b5cf6 75%,
#ec4899 90%,
#ef4444 100%
);
background-size: 200% 200%;
animation: rainbow-shift 8s ease infinite;
}
@keyframes rainbow-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>

View file

@ -0,0 +1,178 @@
---
/**
* LanguageSwitcher - Dropdown for language selection
*
* Usage:
* ```astro
* <LanguageSwitcher
* currentLang="en"
* languages={{ de: 'Deutsch', en: 'English', fr: 'Français', it: 'Italiano', es: 'Español' }}
* getLocalizedPath={(lang) => `/${lang}${currentPath}`}
* />
* ```
*/
export interface LanguageOption {
code: string;
label: string;
}
interface Props {
currentLang: string;
languages: Record<string, string>;
getLocalizedPath?: (lang: string) => string;
class?: string;
}
const {
currentLang,
languages,
getLocalizedPath = (lang) => `/${lang}`,
class: className = '',
} = Astro.props;
const languageEntries = Object.entries(languages);
---
<div class:list={['language-switcher', className]}>
<button class="language-trigger" aria-haspopup="true" aria-expanded="false">
<span class="language-current">{languages[currentLang] || currentLang.toUpperCase()}</span>
<svg class="language-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<div class="language-dropdown" role="menu">
{
languageEntries.map(([code, label]) => (
<a
href={getLocalizedPath(code)}
class:list={['language-option', { 'language-option-active': code === currentLang }]}
role="menuitem"
data-lang={code}
>
{label}
</a>
))
}
</div>
</div>
<style>
.language-switcher {
position: relative;
display: inline-block;
}
.language-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
border: 1px solid var(--color-border);
border-radius: 8px;
color: var(--color-text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.language-trigger:hover {
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
.language-trigger:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-glow);
}
.language-chevron {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
.language-switcher.open .language-chevron {
transform: rotate(180deg);
}
.language-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 140px;
background: var(--color-background-card);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
z-index: 100;
overflow: hidden;
}
.language-switcher.open .language-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.language-option {
display: block;
padding: 10px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: all 0.15s ease;
}
.language-option:hover {
background: var(--color-background-card-hover);
color: var(--color-text-primary);
}
.language-option-active {
color: var(--color-primary);
background: var(--color-primary-glow);
}
.language-option-active:hover {
background: var(--color-primary-glow);
color: var(--color-primary);
}
</style>
<script>
// Toggle dropdown on click
document.querySelectorAll('.language-switcher').forEach((switcher) => {
const trigger = switcher.querySelector('.language-trigger');
trigger?.addEventListener('click', (e) => {
e.stopPropagation();
switcher.classList.toggle('open');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.language-switcher.open').forEach((switcher) => {
switcher.classList.remove('open');
});
});
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.language-switcher.open').forEach((switcher) => {
switcher.classList.remove('open');
});
}
});
</script>

View file

@ -0,0 +1,175 @@
/**
* Shared i18n utilities for landing pages
*/
export * from './types';
import {
type Language,
type CommonTranslations,
type Translations,
languages,
defaultLang,
defaultCommonTranslations,
} from './types';
/**
* Get the language from a URL pathname
* Supports both /en/page and /page patterns
*/
export function getLangFromUrl(url: URL): Language {
const [, lang] = url.pathname.split('/');
if (lang && lang in languages) {
return lang as Language;
}
return defaultLang;
}
/**
* Get the route without the language prefix
*/
export function getRouteFromUrl(url: URL): string {
const pathname = url.pathname;
const [, lang, ...rest] = pathname.split('/');
if (lang && lang in languages) {
return '/' + rest.join('/') || '/';
}
return pathname;
}
/**
* Create a localized path
*/
export function localizePath(path: string, lang: Language): string {
// If the language is the default language, don't add a prefix
if (lang === defaultLang) {
return path;
}
// Ensure path starts with /
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `/${lang}${normalizedPath}`;
}
/**
* Create a translation function for a specific language
*/
export function useTranslations<T extends CommonTranslations>(
lang: Language,
translations: Translations<T>
): (key: keyof T | string) => string {
return function t(key: keyof T | string): string {
const langTranslations = translations[lang];
if (langTranslations && key in langTranslations) {
return langTranslations[key as keyof T] as string;
}
// Fallback to default language
const defaultTranslations = translations[defaultLang];
if (defaultTranslations && key in defaultTranslations) {
return defaultTranslations[key as keyof T] as string;
}
// Return the key as fallback
return String(key);
};
}
/**
* Create a translation function with merged common translations
*/
export function createTranslations<T extends CommonTranslations>(
lang: Language,
appTranslations: Translations<T>
): (key: keyof T | string) => string {
// Merge app translations with common translations
const mergedTranslations = {} as Translations<T>;
for (const l of Object.keys(languages) as Language[]) {
mergedTranslations[l] = {
...defaultCommonTranslations[l],
...appTranslations[l],
} as T;
}
return useTranslations(lang, mergedTranslations);
}
/**
* Get alternate language links for SEO (hreflang)
*/
export function getAlternateLinks(url: URL): Array<{ lang: Language; href: string }> {
const route = getRouteFromUrl(url);
const baseUrl = `${url.protocol}//${url.host}`;
return (Object.keys(languages) as Language[]).map((lang) => ({
lang,
href: `${baseUrl}${localizePath(route, lang)}`,
}));
}
/**
* Detect browser language preference
*/
export function getBrowserLang(): Language {
if (typeof navigator === 'undefined') {
return defaultLang;
}
const browserLang = navigator.language.split('-')[0];
if (browserLang in languages) {
return browserLang as Language;
}
return defaultLang;
}
/**
* Format a date according to the locale
*/
export function formatDate(date: Date, lang: Language): string {
const localeMap: Record<Language, string> = {
de: 'de-DE',
en: 'en-US',
fr: 'fr-FR',
it: 'it-IT',
es: 'es-ES',
};
return date.toLocaleDateString(localeMap[lang], {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
/**
* Format a number according to the locale
*/
export function formatNumber(num: number, lang: Language): string {
const localeMap: Record<Language, string> = {
de: 'de-DE',
en: 'en-US',
fr: 'fr-FR',
it: 'it-IT',
es: 'es-ES',
};
return num.toLocaleString(localeMap[lang]);
}
/**
* Format currency according to the locale
*/
export function formatCurrency(amount: number, lang: Language, currency = 'EUR'): string {
const localeMap: Record<Language, string> = {
de: 'de-DE',
en: 'en-US',
fr: 'fr-FR',
it: 'it-IT',
es: 'es-ES',
};
return new Intl.NumberFormat(localeMap[lang], {
style: 'currency',
currency,
}).format(amount);
}

View file

@ -0,0 +1,241 @@
/**
* Shared i18n types for landing pages
*/
export type Language = 'de' | 'en' | 'fr' | 'it' | 'es';
export const languages: Record<Language, string> = {
de: 'Deutsch',
en: 'English',
fr: 'Français',
it: 'Italiano',
es: 'Español',
};
export const defaultLang: Language = 'de';
/**
* Common translations interface that all apps should implement
* Apps can extend this with their own specific translations
*/
export interface CommonTranslations {
// Navigation
'nav.home'?: string;
'nav.features'?: string;
'nav.pricing'?: string;
'nav.about'?: string;
'nav.contact'?: string;
'nav.login'?: string;
'nav.signup'?: string;
// Buttons
'button.getStarted'?: string;
'button.learnMore'?: string;
'button.tryFree'?: string;
'button.signUp'?: string;
'button.login'?: string;
'button.back'?: string;
'button.submit'?: string;
'button.cancel'?: string;
// Legal
'legal.privacy'?: string;
'legal.terms'?: string;
'legal.cookies'?: string;
'legal.imprint'?: string;
'legal.backHome'?: string;
'legal.lastUpdated'?: string;
// Footer
'footer.product'?: string;
'footer.company'?: string;
'footer.resources'?: string;
'footer.legal'?: string;
'footer.copyright'?: string;
// Common
'common.loading'?: string;
'common.error'?: string;
'common.success'?: string;
'common.new'?: string;
'common.comingSoon'?: string;
// Allow any other string keys
[key: string]: string | undefined;
}
/**
* Type for a translations object mapping languages to translations
*/
export type Translations<T extends CommonTranslations = CommonTranslations> = Record<Language, T>;
/**
* Default common translations in all supported languages
*/
export const defaultCommonTranslations: Translations<CommonTranslations> = {
de: {
'nav.home': 'Startseite',
'nav.features': 'Funktionen',
'nav.pricing': 'Preise',
'nav.about': 'Über uns',
'nav.contact': 'Kontakt',
'nav.login': 'Anmelden',
'nav.signup': 'Registrieren',
'button.getStarted': 'Jetzt starten',
'button.learnMore': 'Mehr erfahren',
'button.tryFree': 'Kostenlos testen',
'button.signUp': 'Registrieren',
'button.login': 'Anmelden',
'button.back': 'Zurück',
'button.submit': 'Absenden',
'button.cancel': 'Abbrechen',
'legal.privacy': 'Datenschutz',
'legal.terms': 'AGB',
'legal.cookies': 'Cookies',
'legal.imprint': 'Impressum',
'legal.backHome': 'Zurück zur Startseite',
'legal.lastUpdated': 'Zuletzt aktualisiert',
'footer.product': 'Produkt',
'footer.company': 'Unternehmen',
'footer.resources': 'Ressourcen',
'footer.legal': 'Rechtliches',
'footer.copyright': 'Alle Rechte vorbehalten.',
'common.loading': 'Laden...',
'common.error': 'Fehler',
'common.success': 'Erfolg',
'common.new': 'Neu',
'common.comingSoon': 'Demnächst',
},
en: {
'nav.home': 'Home',
'nav.features': 'Features',
'nav.pricing': 'Pricing',
'nav.about': 'About',
'nav.contact': 'Contact',
'nav.login': 'Login',
'nav.signup': 'Sign Up',
'button.getStarted': 'Get Started',
'button.learnMore': 'Learn More',
'button.tryFree': 'Try for Free',
'button.signUp': 'Sign Up',
'button.login': 'Login',
'button.back': 'Back',
'button.submit': 'Submit',
'button.cancel': 'Cancel',
'legal.privacy': 'Privacy Policy',
'legal.terms': 'Terms of Service',
'legal.cookies': 'Cookie Policy',
'legal.imprint': 'Imprint',
'legal.backHome': 'Back to Home',
'legal.lastUpdated': 'Last Updated',
'footer.product': 'Product',
'footer.company': 'Company',
'footer.resources': 'Resources',
'footer.legal': 'Legal',
'footer.copyright': 'All rights reserved.',
'common.loading': 'Loading...',
'common.error': 'Error',
'common.success': 'Success',
'common.new': 'New',
'common.comingSoon': 'Coming Soon',
},
fr: {
'nav.home': 'Accueil',
'nav.features': 'Fonctionnalités',
'nav.pricing': 'Tarifs',
'nav.about': 'À propos',
'nav.contact': 'Contact',
'nav.login': 'Connexion',
'nav.signup': "S'inscrire",
'button.getStarted': 'Commencer',
'button.learnMore': 'En savoir plus',
'button.tryFree': 'Essai gratuit',
'button.signUp': "S'inscrire",
'button.login': 'Connexion',
'button.back': 'Retour',
'button.submit': 'Envoyer',
'button.cancel': 'Annuler',
'legal.privacy': 'Confidentialité',
'legal.terms': "Conditions d'utilisation",
'legal.cookies': 'Politique de cookies',
'legal.imprint': 'Mentions légales',
'legal.backHome': "Retour à l'accueil",
'legal.lastUpdated': 'Dernière mise à jour',
'footer.product': 'Produit',
'footer.company': 'Entreprise',
'footer.resources': 'Ressources',
'footer.legal': 'Mentions légales',
'footer.copyright': 'Tous droits réservés.',
'common.loading': 'Chargement...',
'common.error': 'Erreur',
'common.success': 'Succès',
'common.new': 'Nouveau',
'common.comingSoon': 'Bientôt disponible',
},
it: {
'nav.home': 'Home',
'nav.features': 'Funzionalità',
'nav.pricing': 'Prezzi',
'nav.about': 'Chi siamo',
'nav.contact': 'Contatti',
'nav.login': 'Accedi',
'nav.signup': 'Registrati',
'button.getStarted': 'Inizia',
'button.learnMore': 'Scopri di più',
'button.tryFree': 'Prova gratuita',
'button.signUp': 'Registrati',
'button.login': 'Accedi',
'button.back': 'Indietro',
'button.submit': 'Invia',
'button.cancel': 'Annulla',
'legal.privacy': 'Privacy',
'legal.terms': 'Termini di servizio',
'legal.cookies': 'Cookie Policy',
'legal.imprint': 'Imprint',
'legal.backHome': 'Torna alla home',
'legal.lastUpdated': 'Ultimo aggiornamento',
'footer.product': 'Prodotto',
'footer.company': 'Azienda',
'footer.resources': 'Risorse',
'footer.legal': 'Legale',
'footer.copyright': 'Tutti i diritti riservati.',
'common.loading': 'Caricamento...',
'common.error': 'Errore',
'common.success': 'Successo',
'common.new': 'Nuovo',
'common.comingSoon': 'Prossimamente',
},
es: {
'nav.home': 'Inicio',
'nav.features': 'Características',
'nav.pricing': 'Precios',
'nav.about': 'Nosotros',
'nav.contact': 'Contacto',
'nav.login': 'Iniciar sesión',
'nav.signup': 'Registrarse',
'button.getStarted': 'Empezar',
'button.learnMore': 'Saber más',
'button.tryFree': 'Prueba gratis',
'button.signUp': 'Registrarse',
'button.login': 'Iniciar sesión',
'button.back': 'Atrás',
'button.submit': 'Enviar',
'button.cancel': 'Cancelar',
'legal.privacy': 'Privacidad',
'legal.terms': 'Términos de servicio',
'legal.cookies': 'Política de cookies',
'legal.imprint': 'Aviso legal',
'legal.backHome': 'Volver al inicio',
'legal.lastUpdated': 'Última actualización',
'footer.product': 'Producto',
'footer.company': 'Empresa',
'footer.resources': 'Recursos',
'footer.legal': 'Legal',
'footer.copyright': 'Todos los derechos reservados.',
'common.loading': 'Cargando...',
'common.error': 'Error',
'common.success': 'Éxito',
'common.new': 'Nuevo',
'common.comingSoon': 'Próximamente',
},
};

View file

@ -8,8 +8,42 @@
*
* ```astro
* ---
* // Atoms
* import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
* import Badge from '@manacore/shared-landing-ui/atoms/Badge.astro';
* import Card from '@manacore/shared-landing-ui/atoms/Card.astro';
* import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
* import SectionHeader from '@manacore/shared-landing-ui/atoms/SectionHeader.astro';
* import GradientText from '@manacore/shared-landing-ui/atoms/GradientText.astro';
* import LanguageSwitcher from '@manacore/shared-landing-ui/atoms/LanguageSwitcher.astro';
*
* // Sections
* import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro';
* import FeatureSection from '@manacore/shared-landing-ui/sections/FeatureSection.astro';
* import PricingSection from '@manacore/shared-landing-ui/sections/PricingSection.astro';
* import FAQSection from '@manacore/shared-landing-ui/sections/FAQSection.astro';
* import CTASection from '@manacore/shared-landing-ui/sections/CTASection.astro';
* import TestimonialSection from '@manacore/shared-landing-ui/sections/TestimonialSection.astro';
* import StepsSection from '@manacore/shared-landing-ui/sections/StepsSection.astro';
* import AppScrollerSection from '@manacore/shared-landing-ui/sections/AppScrollerSection.astro';
* import TimelineSection from '@manacore/shared-landing-ui/sections/TimelineSection.astro';
* import MasonryGridSection from '@manacore/shared-landing-ui/sections/MasonryGridSection.astro';
* import PrinciplesSection from '@manacore/shared-landing-ui/sections/PrinciplesSection.astro';
*
* // Layouts
* import Footer from '@manacore/shared-landing-ui/layouts/Footer.astro';
* import Navigation from '@manacore/shared-landing-ui/layouts/Navigation.astro';
*
* // Templates
* import LegalPageTemplate from '@manacore/shared-landing-ui/templates/LegalPageTemplate.astro';
*
* // i18n
* import { getLangFromUrl, useTranslations, localizePath } from '@manacore/shared-landing-ui/i18n';
*
* // Themes (import as CSS)
* import '@manacore/shared-landing-ui/themes';
* import '@manacore/shared-landing-ui/themes/manacore';
* import '@manacore/shared-landing-ui/themes/picture';
* ---
* ```
*
@ -18,3 +52,4 @@
*/
export * from './utils/index';
export * from './i18n/index';

View file

@ -0,0 +1,510 @@
---
/**
* Navigation - Shared header navigation component
*
* Usage:
* ```astro
* <Navigation
* brand={{ name: 'MyApp', logo: '/logo.svg', href: '/' }}
* links={[
* { label: 'Features', href: '#features' },
* { label: 'Pricing', href: '/pricing' },
* { label: 'Docs', href: 'https://docs.example.com', external: true }
* ]}
* ctaButton={{ text: 'Get Started', href: '/signup' }}
* showLanguageSwitcher={true}
* currentLang="en"
* languages={{ de: 'Deutsch', en: 'English', fr: 'Français' }}
* />
* ```
*/
export interface NavLink {
label: string;
href: string;
external?: boolean;
}
export interface Brand {
name: string;
logo?: string;
href?: string;
}
export interface CtaButton {
text: string;
href: string;
}
interface Props {
brand: Brand;
links?: NavLink[];
ctaButton?: CtaButton;
showLanguageSwitcher?: boolean;
currentLang?: string;
languages?: Record<string, string>;
getLocalizedPath?: (lang: string) => string;
class?: string;
}
const {
brand,
links = [],
ctaButton,
showLanguageSwitcher = false,
currentLang = 'en',
languages = {},
getLocalizedPath,
class: className = '',
} = Astro.props;
---
<header class:list={['nav-header', className]}>
<div class="nav-container">
<!-- Brand -->
<a href={brand.href || '/'} class="nav-brand">
{
brand.logo ? (
<img src={brand.logo} alt={brand.name} class="nav-logo" />
) : (
<span class="nav-brand-text">{brand.name}</span>
)
}
</a>
<!-- Desktop Navigation -->
<nav class="nav-links">
{
links.map((link) => (
<a
href={link.href}
class="nav-link"
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
>
{link.label}
{link.external && (
<svg class="nav-external-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
)}
</a>
))
}
</nav>
<!-- Right side: Language switcher & CTA -->
<div class="nav-right">
{
showLanguageSwitcher && Object.keys(languages).length > 0 && (
<div class="nav-language">
<button class="language-trigger" aria-haspopup="true" aria-expanded="false">
<span>{languages[currentLang] || currentLang.toUpperCase()}</span>
<svg class="language-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div class="language-dropdown" role="menu">
{Object.entries(languages).map(([code, label]) => (
<a
href={getLocalizedPath ? getLocalizedPath(code) : `/${code}`}
class:list={[
'language-option',
{ 'language-option-active': code === currentLang },
]}
role="menuitem"
>
{label}
</a>
))}
</div>
</div>
)
}
{
ctaButton && (
<a href={ctaButton.href} class="nav-cta">
{ctaButton.text}
</a>
)
}
<!-- Mobile Menu Toggle -->
<button class="nav-mobile-toggle" aria-label="Toggle menu" aria-expanded="false">
<svg class="nav-hamburger" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<svg class="nav-close" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div class="nav-mobile-menu">
<nav class="nav-mobile-links">
{
links.map((link) => (
<a
href={link.href}
class="nav-mobile-link"
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
>
{link.label}
</a>
))
}
</nav>
{
ctaButton && (
<a href={ctaButton.href} class="nav-mobile-cta">
{ctaButton.text}
</a>
)
}
</div>
</header>
<style>
.nav-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--color-background-page);
border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(10px);
background: color-mix(in srgb, var(--color-background-page) 80%, transparent);
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 24px;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
}
.nav-brand {
display: flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
}
.nav-logo {
height: 36px;
width: auto;
}
.nav-brand-text {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.02em;
}
.nav-links {
display: none;
align-items: center;
gap: 8px;
}
@media (min-width: 768px) {
.nav-links {
display: flex;
}
}
.nav-link {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.9375rem;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-link:hover {
color: var(--color-text-primary);
background: var(--color-background-card);
}
.nav-external-icon {
width: 14px;
height: 14px;
opacity: 0.5;
}
.nav-right {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.nav-language {
position: relative;
display: none;
}
@media (min-width: 768px) {
.nav-language {
display: block;
}
}
.language-trigger {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: transparent;
border: 1px solid var(--color-border);
border-radius: 8px;
color: var(--color-text-secondary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
}
.language-trigger:hover {
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
.language-chevron {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
.nav-language.open .language-chevron {
transform: rotate(180deg);
}
.language-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 140px;
background: var(--color-background-card);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
z-index: 100;
overflow: hidden;
}
.nav-language.open .language-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.language-option {
display: block;
padding: 10px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: all 0.15s ease;
}
.language-option:hover {
background: var(--color-background-card-hover);
color: var(--color-text-primary);
}
.language-option-active {
color: var(--color-primary);
background: var(--color-primary-glow);
}
.nav-cta {
display: none;
padding: 10px 20px;
background: var(--color-primary);
color: white;
text-decoration: none;
font-size: 0.9375rem;
font-weight: 600;
border-radius: 8px;
transition: all 0.2s ease;
}
@media (min-width: 640px) {
.nav-cta {
display: inline-flex;
}
}
.nav-cta:hover {
background: var(--color-primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--color-primary-glow);
}
.nav-mobile-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background: transparent;
border: none;
color: var(--color-text-primary);
cursor: pointer;
}
@media (min-width: 768px) {
.nav-mobile-toggle {
display: none;
}
}
.nav-hamburger,
.nav-close {
width: 24px;
height: 24px;
}
.nav-close {
display: none;
}
.nav-header.mobile-open .nav-hamburger {
display: none;
}
.nav-header.mobile-open .nav-close {
display: block;
}
.nav-mobile-menu {
display: none;
padding: 16px 24px 24px;
background: var(--color-background-page);
border-top: 1px solid var(--color-border);
}
.nav-header.mobile-open .nav-mobile-menu {
display: block;
}
@media (min-width: 768px) {
.nav-mobile-menu {
display: none !important;
}
}
.nav-mobile-links {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-mobile-link {
display: block;
padding: 12px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 1rem;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-mobile-link:hover {
color: var(--color-text-primary);
background: var(--color-background-card);
}
.nav-mobile-cta {
display: block;
margin-top: 16px;
padding: 14px 24px;
background: var(--color-primary);
color: white;
text-decoration: none;
font-size: 1rem;
font-weight: 600;
text-align: center;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-mobile-cta:hover {
background: var(--color-primary-hover);
}
</style>
<script>
// Mobile menu toggle
document.querySelectorAll('.nav-mobile-toggle').forEach((toggle) => {
toggle.addEventListener('click', () => {
const header = toggle.closest('.nav-header');
header?.classList.toggle('mobile-open');
});
});
// Language dropdown toggle
document.querySelectorAll('.nav-language .language-trigger').forEach((trigger) => {
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = trigger.closest('.nav-language');
dropdown?.classList.toggle('open');
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.nav-language.open').forEach((dropdown) => {
dropdown.classList.remove('open');
});
});
// Close mobile menu on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.nav-header.mobile-open').forEach((header) => {
header.classList.remove('mobile-open');
});
document.querySelectorAll('.nav-language.open').forEach((dropdown) => {
dropdown.classList.remove('open');
});
}
});
</script>

View file

@ -0,0 +1,475 @@
---
/**
* AppScrollerSection - Horizontal scrolling app showcase
*
* Usage:
* ```astro
* <AppScrollerSection
* title="Discover our"
* titleHighlight="AI Apps"
* subtitle="Access to a growing ecosystem"
* apps={[{ name: 'App', description: '...', logo: '/logo.png', category: 'Productivity', color: 'from-blue-500 to-cyan-500', href: '/app' }]}
* scrollHint="Scroll for more"
* learnMoreText="Learn more"
* />
* ```
*/
export interface App {
name: string;
description: string;
logo: string; // URL starting with '/' or emoji string
category: string;
color: string; // Tailwind gradient classes e.g. 'from-blue-500 to-cyan-500'
href: string;
}
interface Props {
title: string;
titleHighlight?: string;
subtitle?: string;
apps: App[];
scrollHint?: string;
learnMoreText?: string;
class?: string;
}
const {
title,
titleHighlight,
subtitle,
apps,
scrollHint,
learnMoreText = 'Learn more',
class: className = '',
} = Astro.props;
---
<section class:list={['apps-section', className]}>
<div class="apps-container">
<div class="apps-header">
<h2 class="apps-title">
{title}
{titleHighlight && <span class="gradient-text">{titleHighlight}</span>}
</h2>
{subtitle && <p class="apps-subtitle">{subtitle}</p>}
</div>
<div class="apps-scroller">
<div class="apps-track">
{
apps.map((app) => (
<a href={app.href} class="app-card">
<div class="app-card-inner">
<div class="app-icon-wrapper">
<div class={`app-icon-bg bg-gradient-to-br ${app.color}`} />
{app.logo.startsWith('/') ? (
<img src={app.logo} alt={app.name} class="app-logo" loading="lazy" />
) : (
<span class="app-emoji">{app.logo}</span>
)}
</div>
<div class="app-content">
<div class="app-category">{app.category}</div>
<h3 class="app-name">{app.name}</h3>
<p class="app-description">{app.description}</p>
</div>
<div class="app-footer">
<span class="app-button">
{learnMoreText}
<svg class="app-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</span>
</div>
</div>
</a>
))
}
</div>
</div>
{
scrollHint && (
<div class="scroll-hint">
<span>{scrollHint}</span>
<svg class="scroll-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</div>
)
}
</div>
</section>
<style>
.apps-section {
position: relative;
background: linear-gradient(
180deg,
var(--color-background-page) 0%,
var(--color-background-card) 100%
);
padding: 120px 0;
overflow: hidden;
}
.apps-container {
max-width: 100%;
margin: 0 auto;
}
.apps-header {
text-align: center;
margin-bottom: 80px;
padding: 0 80px;
}
.apps-title {
font-size: clamp(2rem, 4vw, 2.75rem);
font-weight: 800;
color: var(--color-text-primary);
margin: 0 0 20px 0;
line-height: 1.2;
}
.gradient-text {
background: linear-gradient(
135deg,
var(--color-primary) 0%,
var(--color-primary-hover) 50%,
var(--color-primary) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
}
.apps-subtitle {
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--color-text-secondary);
margin: 0;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.apps-scroller {
position: relative;
padding: 20px 80px;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: var(--color-primary-glow) var(--color-background-card);
}
.apps-scroller::-webkit-scrollbar {
height: 8px;
}
.apps-scroller::-webkit-scrollbar-track {
background: var(--color-background-card);
border-radius: 4px;
}
.apps-scroller::-webkit-scrollbar-thumb {
background: var(--color-primary-glow);
border-radius: 4px;
transition: background 0.3s ease;
}
.apps-scroller::-webkit-scrollbar-thumb:hover {
background: var(--color-primary);
}
.apps-track {
display: flex;
gap: 30px;
padding-bottom: 20px;
}
.apps-track::after {
content: '';
display: block;
flex-shrink: 0;
width: 80px;
}
.app-card {
flex: 0 0 auto;
width: 320px;
height: 380px;
text-decoration: none;
display: block;
cursor: pointer;
}
.app-card-inner {
height: 100%;
background: linear-gradient(
135deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 30px;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.app-card-inner::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 0%, var(--color-primary-glow) 100%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.app-card:hover .app-card-inner {
transform: translateY(-8px);
border-color: var(--color-primary);
box-shadow:
0 20px 40px -15px rgba(0, 0, 0, 0.5),
0 0 0 1px var(--color-primary-glow);
}
.app-card:hover .app-card-inner::before {
opacity: 0.1;
}
.app-icon-wrapper {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 10px;
}
.app-icon-bg {
position: absolute;
inset: 0;
border-radius: 18px;
opacity: 0.15;
transition: all 0.4s ease;
}
.app-card:hover .app-icon-bg {
opacity: 0.25;
transform: scale(1.1);
}
.app-logo {
position: relative;
width: 100%;
height: 100%;
object-fit: contain;
padding: 12px;
z-index: 1;
}
.app-emoji {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 3rem;
z-index: 1;
}
.app-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
}
.app-category {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-primary);
margin-bottom: 5px;
}
.app-name {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
line-height: 1.3;
}
.app-description {
font-size: 0.9375rem;
line-height: 1.5;
color: var(--color-text-secondary);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.app-footer {
padding-top: 10px;
margin-top: auto;
}
.app-button {
width: 100%;
padding: 0;
height: auto;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-muted);
background: transparent;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
pointer-events: none;
}
.app-card:hover .app-button {
color: var(--color-text-secondary);
}
.app-arrow {
width: 14px;
height: 14px;
transition: transform 0.3s ease;
}
.app-card:hover .app-arrow {
transform: translateX(3px);
}
.scroll-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 60px;
padding: 0 80px;
font-size: 0.875rem;
color: var(--color-text-muted);
animation: pulse 2s ease-in-out infinite;
}
.scroll-arrow {
width: 20px;
height: 20px;
}
@keyframes pulse {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
/* Responsive */
@media (max-width: 1024px) {
.apps-section {
padding: 100px 0;
}
.app-card {
width: 300px;
height: 360px;
}
}
@media (max-width: 768px) {
.apps-section {
padding: 80px 0;
}
.apps-header {
margin-bottom: 60px;
padding: 0 40px;
}
.app-card {
width: 280px;
height: 350px;
}
.apps-scroller {
padding: 20px 40px;
}
.scroll-hint {
padding: 0 40px;
}
}
@media (max-width: 640px) {
.apps-section {
padding: 60px 0;
}
.apps-header {
padding: 0 30px;
}
.app-card {
width: 260px;
height: 340px;
}
.app-card-inner {
padding: 24px;
}
.apps-scroller {
padding: 20px 30px;
}
.scroll-hint {
margin-top: 40px;
padding: 0 30px;
font-size: 0.8125rem;
}
}
@media (max-width: 400px) {
.app-card {
width: 240px;
height: 330px;
}
.app-icon-wrapper {
width: 70px;
height: 70px;
}
.app-emoji {
font-size: 2.5rem;
}
.app-name {
font-size: 1.25rem;
}
.app-description {
font-size: 0.875rem;
}
}
</style>

View file

@ -0,0 +1,235 @@
---
/**
* MasonryGridSection - Grid layout with variable sized cards
*
* Usage:
* ```astro
* <MasonryGridSection
* title="Why we do this"
* subtitle="Our guiding principles"
* items={[
* { number: '01', title: 'Technology for all', text: '...', size: 'large' },
* { number: '02', title: 'Transparent', text: '...', size: 'medium' }
* ]}
* />
* ```
*/
export interface MasonryItem {
number?: string;
title: string;
text: string;
size?: 'small' | 'medium' | 'large';
}
interface Props {
title: string;
subtitle?: string;
items: MasonryItem[];
class?: string;
}
const { title, subtitle, items, class: className = '' } = Astro.props;
---
<section class:list={['masonry-section', className]}>
<div class="masonry-overlay"></div>
<div class="masonry-container">
<div class="masonry-header">
<h2 class="masonry-main-title">{title}</h2>
{subtitle && <p class="masonry-main-subtitle">{subtitle}</p>}
</div>
<div class="masonry-grid">
{
items.map((item) => (
<div class={`masonry-item masonry-${item.size || 'medium'}`}>
<div class="masonry-content">
{item.number && <span class="masonry-number">{item.number}</span>}
<h4 class="masonry-title">{item.title}</h4>
<p class="masonry-text">{item.text}</p>
</div>
</div>
))
}
</div>
</div>
</section>
<style>
.masonry-section {
position: relative;
background: linear-gradient(
180deg,
var(--color-background-page) 0%,
var(--color-background-card) 100%
);
padding: 160px 0;
overflow: hidden;
}
.masonry-overlay {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 30%, var(--color-primary-glow) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, var(--color-primary-glow) 0%, transparent 50%);
opacity: 0.2;
pointer-events: none;
}
.masonry-container {
position: relative;
max-width: 1400px;
margin: 0 auto;
padding: 0 80px;
z-index: 10;
}
.masonry-header {
text-align: center;
margin-bottom: 120px;
}
.masonry-main-title {
font-size: clamp(2.5rem, 5vw, 3.5rem);
font-weight: 800;
color: var(--color-text-primary);
margin: 0 0 20px 0;
line-height: 1.1;
letter-spacing: -0.02em;
}
.masonry-main-subtitle {
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--color-text-muted);
margin: 0;
}
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
grid-auto-flow: dense;
}
.masonry-item {
position: relative;
transition: transform 0.3s ease;
}
.masonry-item:hover {
transform: translateY(-4px);
}
.masonry-large {
grid-column: span 2;
}
.masonry-medium {
grid-column: span 1;
}
.masonry-small {
grid-column: span 1;
}
.masonry-content {
height: 100%;
background: linear-gradient(
135deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
border: 1px solid var(--color-primary-glow);
border-radius: 20px;
padding: 40px;
display: flex;
flex-direction: column;
gap: 20px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.masonry-item:hover .masonry-content {
border-color: var(--color-primary);
box-shadow: 0 8px 32px var(--color-primary-glow);
}
.masonry-number {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
opacity: 0.3;
}
.masonry-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
line-height: 1.3;
}
.masonry-text {
font-size: 1rem;
line-height: 1.6;
color: var(--color-text-secondary);
margin: 0;
}
@media (max-width: 768px) {
.masonry-section {
padding: 120px 0;
}
.masonry-container {
padding: 0 40px;
}
.masonry-header {
margin-bottom: 80px;
}
.masonry-large,
.masonry-medium,
.masonry-small {
grid-column: span 1;
}
.masonry-content {
padding: 30px;
}
}
@media (max-width: 640px) {
.masonry-section {
padding: 80px 0;
}
.masonry-container {
padding: 0 24px;
}
.masonry-grid {
grid-template-columns: 1fr;
}
.masonry-content {
padding: 24px;
}
.masonry-number {
font-size: 2.5rem;
}
.masonry-title {
font-size: 1.25rem;
}
}
</style>

View file

@ -0,0 +1,280 @@
---
/**
* PrinciplesSection - Horizontal cards with icons
*
* Usage:
* ```astro
* <PrinciplesSection
* principles={[
* { icon: 'lightning', title: 'Scalable with AI', description: '...' },
* { icon: 'chat', title: 'Tight Feedback Loop', description: '...' }
* ]}
* />
* ```
*/
export interface Principle {
icon:
| 'lightning'
| 'chat'
| 'globe'
| 'heart'
| 'shield'
| 'star'
| 'code'
| 'users'
| 'rocket'
| 'check';
title: string;
description: string;
}
interface Props {
principles: Principle[];
class?: string;
}
const { principles, class: className = '' } = Astro.props;
// SVG paths for icons
const iconPaths: Record<string, string> = {
lightning: 'M13 10V3L4 14h7v7l9-11h-7z',
chat: 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
globe:
'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
heart:
'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
shield:
'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
star: 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z',
code: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
users:
'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
rocket:
'M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z',
check: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
};
---
<section class:list={['principles-section', className]}>
<div class="principles-overlay"></div>
<div class="principles-container">
<div class="principles-grid">
{
principles.map((principle) => (
<div class="principle-card">
<div class="principle-card-inner">
<div class="principle-icon-wrapper">
<svg class="principle-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={iconPaths[principle.icon] || iconPaths.star}
/>
</svg>
</div>
<div class="principle-content">
<h3 class="principle-title">{principle.title}</h3>
<p class="principle-description">{principle.description}</p>
</div>
</div>
</div>
))
}
</div>
</div>
</section>
<style>
.principles-section {
position: relative;
background: linear-gradient(
180deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
padding: 160px 0;
overflow: hidden;
}
.principles-overlay {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 30% 50%, var(--color-primary-glow) 0%, transparent 50%),
radial-gradient(circle at 70% 50%, var(--color-primary-glow) 0%, transparent 50%);
opacity: 0.3;
pointer-events: none;
}
.principles-container {
position: relative;
max-width: 1400px;
margin: 0 auto;
padding: 0 80px;
z-index: 10;
}
.principles-grid {
display: grid;
grid-template-columns: 1fr;
gap: 30px;
max-width: 1200px;
margin: 0 auto;
}
.principle-card {
position: relative;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.principle-card:hover {
transform: translateY(-8px);
}
.principle-card-inner {
height: 100%;
background: linear-gradient(
135deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
border: 1px solid var(--color-border);
border-radius: 24px;
padding: 50px;
display: grid;
grid-template-columns: auto 1fr;
gap: 40px;
align-items: center;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.principle-card-inner::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 0%, var(--color-primary-glow) 100%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.principle-card:hover .principle-card-inner {
border-color: var(--color-primary);
box-shadow:
0 20px 40px -15px rgba(0, 0, 0, 0.5),
0 0 0 1px var(--color-primary-glow);
}
.principle-card:hover .principle-card-inner::before {
opacity: 0.1;
}
.principle-icon-wrapper {
width: 100px;
height: 100px;
flex-shrink: 0;
background: linear-gradient(
135deg,
var(--color-primary-glow) 0%,
var(--color-primary-glow) 100%
);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 1;
}
.principle-card:hover .principle-icon-wrapper {
background: linear-gradient(135deg, var(--color-primary-glow) 0%, var(--color-primary) 100%);
transform: scale(1.1) rotate(-5deg);
}
.principle-icon {
width: 48px;
height: 48px;
color: var(--color-primary);
transition: all 0.4s ease;
}
.principle-card:hover .principle-icon {
color: var(--color-text-primary);
filter: drop-shadow(0 0 8px var(--color-primary));
}
.principle-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 40px;
align-items: center;
z-index: 1;
}
.principle-title {
font-size: clamp(1.75rem, 3vw, 2.25rem);
font-weight: 800;
color: var(--color-text-primary);
margin: 0;
line-height: 1.2;
letter-spacing: -0.02em;
}
.principle-description {
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.7;
color: var(--color-text-secondary);
margin: 0;
}
@media (max-width: 1200px) {
.principle-content {
grid-template-columns: 1fr;
gap: 15px;
}
}
@media (max-width: 1024px) {
.principles-section {
padding: 120px 0;
}
}
@media (max-width: 640px) {
.principles-section {
padding: 80px 0;
}
.principles-container {
padding: 0 40px;
}
.principle-card-inner {
grid-template-columns: 1fr;
text-align: center;
padding: 40px;
}
.principle-icon-wrapper {
width: 80px;
height: 80px;
margin: 0 auto;
}
.principle-icon {
width: 40px;
height: 40px;
}
.principle-content {
align-items: center;
}
}
</style>

View file

@ -0,0 +1,286 @@
---
/**
* TimelineSection - Alternating timeline layout
*
* Usage:
* ```astro
* <TimelineSection
* title="Our Journey"
* subtitle="From vision to reality"
* items={[
* { badge: 'Mission', title: 'Democratizing AI', text: 'Breaking barriers...' },
* { badge: 'Vision', title: 'A connected future', text: 'A world where...' }
* ]}
* />
* ```
*/
export interface TimelineItem {
badge: string;
title: string;
text: string;
}
interface Props {
title: string;
subtitle?: string;
items: TimelineItem[];
class?: string;
}
const { title, subtitle, items, class: className = '' } = Astro.props;
---
<section class:list={['timeline-section', className]}>
<div class="timeline-overlay"></div>
<div class="timeline-container">
<div class="timeline-header">
<h2 class="timeline-main-title">{title}</h2>
{subtitle && <p class="timeline-main-subtitle">{subtitle}</p>}
</div>
<div class="timeline-grid">
{
items.map((item, index) => (
<div class={`timeline-item ${index % 2 === 0 ? 'timeline-left' : 'timeline-right'}`}>
<div class="timeline-dot" />
<div class="timeline-content">
<span class="timeline-badge">{item.badge}</span>
<h4 class="timeline-title">{item.title}</h4>
<p class="timeline-text">{item.text}</p>
</div>
</div>
))
}
</div>
</div>
</section>
<style>
.timeline-section {
position: relative;
background: linear-gradient(
180deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
padding: 160px 0;
overflow: hidden;
}
.timeline-overlay {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 30% 50%, var(--color-primary-glow) 0%, transparent 50%),
radial-gradient(circle at 70% 50%, var(--color-primary-glow) 0%, transparent 50%);
opacity: 0.3;
pointer-events: none;
}
.timeline-container {
position: relative;
max-width: 1400px;
margin: 0 auto;
padding: 0 80px;
z-index: 10;
}
.timeline-header {
text-align: center;
margin-bottom: 120px;
}
.timeline-main-title {
font-size: clamp(2.5rem, 5vw, 3.5rem);
font-weight: 800;
color: var(--color-text-primary);
margin: 0 0 20px 0;
line-height: 1.1;
letter-spacing: -0.02em;
}
.timeline-main-subtitle {
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--color-text-muted);
margin: 0;
}
.timeline-grid {
position: relative;
display: flex;
flex-direction: column;
gap: 60px;
max-width: 1000px;
margin: 0 auto;
}
.timeline-grid::before {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(
180deg,
transparent 0%,
var(--color-primary) 10%,
var(--color-primary) 90%,
transparent 100%
);
opacity: 0.5;
transform: translateX(-50%);
}
.timeline-item {
position: relative;
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 40px;
align-items: center;
}
.timeline-left .timeline-content {
grid-column: 1;
text-align: right;
}
.timeline-right .timeline-content {
grid-column: 3;
text-align: left;
}
.timeline-dot {
grid-column: 2;
width: 20px;
height: 20px;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
border-radius: 50%;
box-shadow:
0 0 0 4px var(--color-primary-glow),
0 0 20px var(--color-primary-glow);
z-index: 2;
transition: all 0.3s ease;
}
.timeline-item:hover .timeline-dot {
box-shadow:
0 0 0 6px var(--color-primary-glow),
0 0 30px var(--color-primary);
transform: scale(1.2);
}
.timeline-content {
background: linear-gradient(
135deg,
var(--color-background-card) 0%,
var(--color-background-page) 100%
);
border: 1px solid var(--color-border);
border-radius: 16px;
padding: 30px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.timeline-content::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, transparent 0%, var(--color-primary-glow) 100%);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.timeline-item:hover .timeline-content {
border-color: var(--color-primary);
transform: scale(1.02);
box-shadow:
0 10px 30px -10px rgba(0, 0, 0, 0.5),
0 0 0 1px var(--color-primary-glow);
}
.timeline-item:hover .timeline-content::before {
opacity: 0.1;
}
.timeline-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 10px;
}
.timeline-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 0 0 10px 0;
line-height: 1.3;
}
.timeline-text {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--color-text-secondary);
margin: 0;
}
/* Mobile: Switch to left-aligned layout */
@media (max-width: 768px) {
.timeline-section {
padding: 120px 0;
}
.timeline-container {
padding: 0 40px;
}
.timeline-header {
margin-bottom: 80px;
}
.timeline-grid::before {
left: 20px;
}
.timeline-item {
grid-template-columns: auto 1fr;
gap: 20px;
}
.timeline-dot {
grid-column: 1;
grid-row: 1;
}
.timeline-left .timeline-content,
.timeline-right .timeline-content {
grid-column: 2;
grid-row: 1;
text-align: left;
}
}
@media (max-width: 640px) {
.timeline-section {
padding: 80px 0;
}
.timeline-container {
padding: 0 40px;
}
.timeline-content {
padding: 20px;
}
}
</style>

View file

@ -0,0 +1,225 @@
---
/**
* LegalPageTemplate - Base template for legal pages (Privacy, Terms, Cookies, Imprint)
*
* Usage:
* ```astro
* <LegalPageTemplate
* title="Privacy Policy"
* backLink="/"
* backText="Back to Home"
* lastUpdatedText="Last Updated"
* lastUpdated={new Date()}
* >
* <div set:html={content} />
* </LegalPageTemplate>
* ```
*/
interface Props {
title: string;
backLink?: string;
backText?: string;
lastUpdatedText?: string;
lastUpdated?: Date;
class?: string;
}
const {
title,
backLink = '/',
backText = 'Back to Home',
lastUpdatedText = 'Last Updated',
lastUpdated = new Date(),
class: className = '',
} = Astro.props;
---
<div class:list={['legal-page', className]}>
<div class="legal-container">
<!-- Back Button -->
<a href={backLink} class="legal-back-link">
<svg class="legal-back-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"
></path>
</svg>
<span>{backText}</span>
</a>
<!-- Page Header -->
<div class="legal-header">
<h1 class="legal-title">{title}</h1>
<p class="legal-date">{lastUpdatedText}: {lastUpdated.toLocaleDateString()}</p>
</div>
<!-- Content -->
<div class="legal-prose">
<div class="legal-content">
<slot />
</div>
</div>
</div>
</div>
<style>
.legal-page {
min-height: 100vh;
background: var(--color-background-page);
padding: 96px 0;
}
.legal-container {
max-width: 896px;
margin: 0 auto;
padding: 0 16px;
}
.legal-back-link {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
color: var(--color-primary);
text-decoration: none;
transition: color 0.2s ease;
}
.legal-back-link:hover {
color: var(--color-primary-hover);
}
.legal-back-icon {
width: 20px;
height: 20px;
}
.legal-header {
margin-bottom: 48px;
}
.legal-title {
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 700;
color: var(--color-text-primary);
margin: 0 0 16px 0;
}
.legal-date {
color: var(--color-text-muted);
margin: 0;
}
.legal-content {
background: var(--color-background-card);
border: 1px solid var(--color-border);
border-radius: 16px;
padding: 32px;
}
@media (min-width: 768px) {
.legal-content {
padding: 48px;
}
}
/* Prose styling for content */
:global(.legal-prose) {
color: var(--color-text-secondary);
}
:global(.legal-prose h2) {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
margin-top: 32px;
margin-bottom: 16px;
}
:global(.legal-prose h2:first-child) {
margin-top: 0;
}
:global(.legal-prose h3) {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin-top: 24px;
margin-bottom: 12px;
}
:global(.legal-prose p) {
margin-bottom: 16px;
line-height: 1.7;
color: var(--color-text-secondary);
}
:global(.legal-prose ul),
:global(.legal-prose ol) {
margin-bottom: 16px;
padding-left: 24px;
}
:global(.legal-prose ul) {
list-style-type: disc;
}
:global(.legal-prose ol) {
list-style-type: decimal;
}
:global(.legal-prose li) {
margin-bottom: 8px;
line-height: 1.6;
color: var(--color-text-secondary);
}
:global(.legal-prose a) {
color: var(--color-primary);
text-decoration: none;
transition: color 0.2s ease;
}
:global(.legal-prose a:hover) {
color: var(--color-primary-hover);
}
:global(.legal-prose strong) {
font-weight: 600;
color: var(--color-text-primary);
}
:global(.legal-prose blockquote) {
border-left: 4px solid var(--color-primary);
padding-left: 16px;
margin: 24px 0;
font-style: italic;
color: var(--color-text-muted);
}
:global(.legal-prose code) {
background: var(--color-background-page);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875em;
font-family: monospace;
}
:global(.legal-prose table) {
width: 100%;
border-collapse: collapse;
margin: 24px 0;
}
:global(.legal-prose th),
:global(.legal-prose td) {
padding: 12px;
border: 1px solid var(--color-border);
text-align: left;
}
:global(.legal-prose th) {
background: var(--color-background-page);
font-weight: 600;
color: var(--color-text-primary);
}
</style>

View file

@ -0,0 +1,80 @@
/**
* Chat Theme - Blue
* AI chat application
*/
:root {
/* Primary colors - Vibrant Blue */
--color-primary: #0ea5e9;
--color-primary-hover: #0284c7;
--color-primary-glow: rgba(14, 165, 233, 0.3);
/* Text colors */
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
/* Background colors - Dark slate */
--color-background-page: #0f172a;
--color-background-card: #1e293b;
--color-background-card-hover: #334155;
/* Border colors */
--color-border: rgba(14, 165, 233, 0.2);
--color-border-hover: rgba(14, 165, 233, 0.4);
}
/* Light mode support */
@media (prefers-color-scheme: light) {
:root {
--color-primary: #0284c7;
--color-primary-hover: #0369a1;
--color-primary-glow: rgba(2, 132, 199, 0.2);
--color-text-primary: #0c4a6e;
--color-text-secondary: #0369a1;
--color-text-muted: #0ea5e9;
--color-background-page: #f0f9ff;
--color-background-card: #ffffff;
--color-background-card-hover: #e0f2fe;
--color-border: #bae6fd;
--color-border-hover: #7dd3fc;
}
}
/* Force dark mode class */
.dark {
--color-primary: #0ea5e9;
--color-primary-hover: #38bdf8;
--color-primary-glow: rgba(14, 165, 233, 0.3);
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
--color-background-page: #0f172a;
--color-background-card: #1e293b;
--color-background-card-hover: #334155;
--color-border: rgba(14, 165, 233, 0.2);
--color-border-hover: rgba(14, 165, 233, 0.4);
}
/* Force light mode class */
.light {
--color-primary: #0284c7;
--color-primary-hover: #0369a1;
--color-primary-glow: rgba(2, 132, 199, 0.2);
--color-text-primary: #0c4a6e;
--color-text-secondary: #0369a1;
--color-text-muted: #0ea5e9;
--color-background-page: #f0f9ff;
--color-background-card: #ffffff;
--color-background-card-hover: #e0f2fe;
--color-border: #bae6fd;
--color-border-hover: #7dd3fc;
}

View file

@ -0,0 +1,80 @@
/**
* Picture Theme - Dark Indigo
* AI image generation app
*/
:root {
/* Primary colors - Indigo/Purple */
--color-primary: #6366f1;
--color-primary-hover: #4f46e5;
--color-primary-glow: rgba(99, 102, 241, 0.3);
/* Text colors */
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
/* Background colors - Deep dark */
--color-background-page: #0c0a1d;
--color-background-card: #1a1833;
--color-background-card-hover: #252345;
/* Border colors */
--color-border: rgba(99, 102, 241, 0.2);
--color-border-hover: rgba(99, 102, 241, 0.4);
}
/* Light mode support */
@media (prefers-color-scheme: light) {
:root {
--color-primary: #4f46e5;
--color-primary-hover: #4338ca;
--color-primary-glow: rgba(79, 70, 229, 0.2);
--color-text-primary: #1e1b4b;
--color-text-secondary: #4338ca;
--color-text-muted: #6366f1;
--color-background-page: #fafafa;
--color-background-card: #ffffff;
--color-background-card-hover: #f5f3ff;
--color-border: #e0e7ff;
--color-border-hover: #c7d2fe;
}
}
/* Force dark mode class */
.dark {
--color-primary: #6366f1;
--color-primary-hover: #818cf8;
--color-primary-glow: rgba(99, 102, 241, 0.3);
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
--color-background-page: #0c0a1d;
--color-background-card: #1a1833;
--color-background-card-hover: #252345;
--color-border: rgba(99, 102, 241, 0.2);
--color-border-hover: rgba(99, 102, 241, 0.4);
}
/* Force light mode class */
.light {
--color-primary: #4f46e5;
--color-primary-hover: #4338ca;
--color-primary-glow: rgba(79, 70, 229, 0.2);
--color-text-primary: #1e1b4b;
--color-text-secondary: #4338ca;
--color-text-muted: #6366f1;
--color-background-page: #fafafa;
--color-background-card: #ffffff;
--color-background-card-hover: #f5f3ff;
--color-border: #e0e7ff;
--color-border-hover: #c7d2fe;
}

View file

@ -0,0 +1,80 @@
/**
* Zitare Theme - Green/Teal
* Daily inspiration quotes app
*/
:root {
/* Primary colors - Teal/Emerald */
--color-primary: #14b8a6;
--color-primary-hover: #0d9488;
--color-primary-glow: rgba(20, 184, 166, 0.3);
/* Text colors */
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
/* Background colors - Dark with teal tint */
--color-background-page: #0a1a18;
--color-background-card: #132d2a;
--color-background-card-hover: #1a3f3b;
/* Border colors */
--color-border: rgba(20, 184, 166, 0.2);
--color-border-hover: rgba(20, 184, 166, 0.4);
}
/* Light mode support */
@media (prefers-color-scheme: light) {
:root {
--color-primary: #0d9488;
--color-primary-hover: #0f766e;
--color-primary-glow: rgba(13, 148, 136, 0.2);
--color-text-primary: #134e4a;
--color-text-secondary: #0f766e;
--color-text-muted: #14b8a6;
--color-background-page: #f0fdfa;
--color-background-card: #ffffff;
--color-background-card-hover: #ccfbf1;
--color-border: #99f6e4;
--color-border-hover: #5eead4;
}
}
/* Force dark mode class */
.dark {
--color-primary: #14b8a6;
--color-primary-hover: #2dd4bf;
--color-primary-glow: rgba(20, 184, 166, 0.3);
--color-text-primary: #f8fafc;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
--color-background-page: #0a1a18;
--color-background-card: #132d2a;
--color-background-card-hover: #1a3f3b;
--color-border: rgba(20, 184, 166, 0.2);
--color-border-hover: rgba(20, 184, 166, 0.4);
}
/* Force light mode class */
.light {
--color-primary: #0d9488;
--color-primary-hover: #0f766e;
--color-primary-glow: rgba(13, 148, 136, 0.2);
--color-text-primary: #134e4a;
--color-text-secondary: #0f766e;
--color-text-muted: #14b8a6;
--color-background-page: #f0fdfa;
--color-background-card: #ffffff;
--color-background-card-hover: #ccfbf1;
--color-border: #99f6e4;
--color-border-hover: #5eead4;
}