feat(landing): add shared-landing-ui package and manadeck landing page

- Create @manacore/shared-landing-ui package with reusable components
  (FeatureSection, StepsSection, FAQSection, CTASection, Card atoms)
- Add complete landing page for manadeck app
- Refactor märchenzauber landing to use shared components
  (remove local CTA, FAQ, Features, HowItWorks sections)
- Add German localization for manacore and memoro landing pages
- Update workspace configuration and package dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 03:03:41 +01:00
parent 4bdb5dea85
commit c6c4c5a552
52 changed files with 3519 additions and 381 deletions

View file

@ -0,0 +1,326 @@
# @manacore/shared-landing-ui
Shared Astro components for landing pages across the Manacore monorepo.
## Installation
```bash
pnpm add @manacore/shared-landing-ui
```
## Usage
Import components directly from their paths:
```astro
---
import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
import Card from '@manacore/shared-landing-ui/atoms/Card.astro';
import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro';
import FeatureSection from '@manacore/shared-landing-ui/sections/FeatureSection.astro';
import FAQSection from '@manacore/shared-landing-ui/sections/FAQSection.astro';
---
```
## Required CSS Variables
The components use CSS custom properties for theming. Define these in your project's global CSS:
```css
:root {
/* Primary colors */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-glow: rgba(59, 130, 246, 0.3);
/* Text colors */
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #111827;
--color-background-card: #1f2937;
--color-background-card-hover: #374151;
/* Border colors */
--color-border: #374151;
--color-border-hover: #4b5563;
}
```
## Components
### Atoms (Basic Building Blocks)
- **Button** - Versatile button/link component with variants (primary, secondary, outline, ghost)
- **Card** - Container component with variants (default, hover, glow, bordered)
- **Badge** - Small label component with color variants
- **Container** - Max-width wrapper with responsive padding
- **SectionHeader** - Consistent section title and subtitle
### Sections (Page Sections)
- **HeroSection** - Hero area with title, subtitle, CTAs, and optional image
- **FeatureSection** - Feature grid with icons and descriptions
- **FAQSection** - Expandable FAQ accordion
- **TestimonialSection** - Customer testimonials grid
- **CTASection** - Call-to-action section
- **PricingSection** - Pricing plans comparison
### Layouts
- **Footer** - Configurable footer with sections, social links, and CTAs
## Examples
### Hero Section
```astro
<HeroSection
title="Welcome to Our App"
subtitle="The best solution for your needs"
variant="default"
image={{
src: "/hero-image.jpg",
alt: "Hero image",
position: "right"
}}
primaryCta={{
text: "Get Started",
href: "/signup"
}}
secondaryCta={{
text: "Learn More",
href: "#features",
variant: "secondary"
}}
trustBadges={[
{ icon: "✓", text: "Free Trial" },
{ icon: "🔒", text: "Secure" }
]}
/>
```
### Feature Section
```astro
<FeatureSection
id="features"
title="Amazing Features"
subtitle="Everything you need to succeed"
columns={3}
features={[
{
icon: "🚀",
title: "Fast",
description: "Lightning-fast performance"
},
{
icon: "🔒",
title: "Secure",
description: "Enterprise-grade security"
},
{
icon: "💡",
title: "Smart",
description: "AI-powered insights"
}
]}
/>
```
### FAQ Section
```astro
<FAQSection
title="Frequently Asked Questions"
faqs={[
{
question: "How does it work?",
answer: "It's simple! Just sign up and start using our platform."
},
{
question: "Is there a free trial?",
answer: "Yes, we offer a 14-day free trial with full features."
}
]}
/>
```
## Customization
### Slots
Most section components support slots for additional customization:
```astro
<HeroSection title="..." subtitle="...">
<Fragment slot="title">
Custom <span class="text-gradient">Title</span>
</Fragment>
<Fragment slot="background">
<div class="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 opacity-10" />
</Fragment>
</HeroSection>
```
### Class Overrides
All components accept a `class` prop for additional styling:
```astro
<Button class="my-custom-class">Click me</Button>
```
## Pre-built Themes
Use one of the pre-built theme CSS files for quick setup:
```css
/* In your global CSS or layout */
@import '@manacore/shared-landing-ui/themes/memoro';
/* OR */
@import '@manacore/shared-landing-ui/themes/manacore';
/* OR */
@import '@manacore/shared-landing-ui/themes/maerchenzauber';
/* OR */
@import '@manacore/shared-landing-ui/themes/manadeck';
```
Or import in your Astro layout:
```astro
---
import '@manacore/shared-landing-ui/themes/memoro';
---
```
## Migration Guide
### Step 1: Add the package
```bash
# In your landing app directory
pnpm add @manacore/shared-landing-ui
```
Or add to package.json:
```json
{
"dependencies": {
"@manacore/shared-landing-ui": "workspace:*"
}
}
```
### Step 2: Add CSS Variables
Either import a pre-built theme (see above) or add the CSS variables to your global styles:
```css
:root {
--color-primary: #your-primary-color;
--color-primary-hover: #your-primary-hover;
--color-primary-glow: rgba(your-primary-rgb, 0.3);
--color-text-primary: #text-color;
--color-text-secondary: #secondary-text;
--color-text-muted: #muted-text;
--color-background-page: #page-bg;
--color-background-card: #card-bg;
--color-background-card-hover: #card-hover-bg;
--color-border: #border-color;
--color-border-hover: #border-hover;
}
```
### Step 3: Replace Components
Replace your existing components with shared ones:
**Before (custom component):**
```astro
---
import HeroSection from '../components/sections/Hero.astro';
---
<HeroSection />
```
**After (shared component):**
```astro
---
import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro';
---
<HeroSection
title="Your Title"
subtitle="Your subtitle"
primaryCta={{ text: "Get Started", href: "/start" }}
/>
```
### Step 4: Migrate Data
Move hardcoded content to props:
**Before:**
```astro
<!-- Features hardcoded in component -->
const features = [
{ icon: '🚀', title: 'Fast', description: '...' }
];
```
**After:**
```astro
---
// Data in page file
const features = [
{ icon: '🚀', title: 'Fast', description: '...' }
];
---
<FeatureSection features={features} title="Features" />
```
### Example Demo Pages
Check these demo pages for working examples:
- `maerchenzauber/apps/landing/src/pages/shared-demo.astro`
- `memoro/apps/landing/src/pages/de/shared-demo.astro`
- `manacore/apps/landing/src/pages/de/shared-demo.astro`
## Development
```bash
# Type check
pnpm run type-check
```
## File Structure
```
src/
├── atoms/ # Basic UI components
│ ├── Button.astro
│ ├── Card.astro
│ ├── Badge.astro
│ ├── Container.astro
│ └── SectionHeader.astro
├── sections/ # Page sections
│ ├── HeroSection.astro
│ ├── FeatureSection.astro
│ ├── FAQSection.astro
│ ├── TestimonialSection.astro
│ ├── CTASection.astro
│ └── PricingSection.astro
├── layouts/
│ └── Footer.astro
├── themes/ # Pre-built CSS themes
│ ├── index.css # Default theme
│ ├── memoro.css
│ ├── manacore.css
│ ├── maerchenzauber.css
│ └── manadeck.css
└── utils/
└── index.ts # TypeScript types
```

View file

@ -0,0 +1,39 @@
{
"name": "@manacore/shared-landing-ui",
"version": "0.1.0",
"description": "Shared Astro landing page components for Manacore monorepo",
"type": "module",
"exports": {
".": "./src/index.ts",
"./atoms/*": "./src/atoms/*",
"./sections/*": "./src/sections/*",
"./layouts/*": "./src/layouts/*",
"./utils": "./src/utils/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"
},
"files": [
"src"
],
"scripts": {
"type-check": "astro check"
},
"peerDependencies": {
"astro": ">=5.0.0",
"astro-icon": ">=1.0.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"astro": "^5.16.0",
"typescript": "^5.0.0"
},
"keywords": [
"astro",
"landing",
"components",
"manacore"
]
}

View file

@ -0,0 +1,36 @@
---
/**
* Shared Badge component for landing pages
*/
interface Props {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
size?: 'sm' | 'md' | 'lg';
class?: string;
}
const {
variant = 'default',
size = 'md',
class: className = ''
} = Astro.props;
const variants = {
default: 'bg-[var(--color-background-card)] text-[var(--color-text-secondary)] border-[var(--color-border)]',
primary: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)] border-[var(--color-primary)]/20',
success: 'bg-green-100 text-green-700 border-green-200',
warning: 'bg-yellow-100 text-yellow-700 border-yellow-200',
error: 'bg-red-100 text-red-700 border-red-200'
};
const sizes = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-2.5 py-1',
lg: 'text-base px-3 py-1.5'
};
const styles = `inline-flex items-center rounded-full font-medium border ${variants[variant]} ${sizes[size]} ${className}`;
---
<span class={styles}>
<slot />
</span>

View file

@ -0,0 +1,75 @@
---
/**
* Shared Button component for landing pages
* Uses CSS custom properties for theming - define these in your project:
* --color-primary, --color-primary-hover, --color-text-primary,
* --color-background-card, --color-border
*/
interface Props {
href?: string;
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
class?: string;
target?: string;
rel?: string;
download?: boolean | string;
id?: string;
onclick?: string;
'aria-label'?: string;
fullWidth?: boolean;
[key: string]: any;
}
const {
href,
variant = "primary",
size = "md",
class: className = "",
target,
rel,
download,
id,
onclick,
'aria-label': ariaLabel,
fullWidth = false,
...rest
} = Astro.props;
const variants = {
primary:
"bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)] border-2 border-[var(--color-primary)] hover:border-[var(--color-primary-hover)]",
secondary:
"bg-transparent text-[var(--color-text-primary)] hover:bg-[var(--color-background-card)] border-2 border-[var(--color-border)] hover:border-[var(--color-primary)]",
outline:
"bg-transparent text-[var(--color-text-primary)] hover:bg-[var(--color-background-card-hover)] border-2 border-[var(--color-border)]",
ghost:
"bg-transparent text-[var(--color-text-primary)] hover:bg-[var(--color-background-card-hover)] border-transparent",
};
const sizes = {
sm: "text-sm px-3 py-1.5",
md: "text-base px-4 py-2",
lg: "text-lg px-6 py-3",
};
const baseStyles =
"inline-flex items-center justify-center rounded-lg transition-all duration-200 font-medium focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 whitespace-nowrap";
const widthStyle = fullWidth ? "w-full" : "";
const styles = `${baseStyles} ${variants[variant]} ${sizes[size]} ${widthStyle} ${className}`;
const Element = href ? "a" : "button";
---
<Element
href={href}
target={target}
rel={rel}
download={download}
class={styles}
id={id}
onclick={onclick}
aria-label={ariaLabel}
{...rest}
>
<slot />
</Element>

View file

@ -0,0 +1,50 @@
---
/**
* Shared Card component for landing pages
* Uses CSS custom properties for theming - define these in your project:
* --color-background-card, --color-border, --color-primary
*/
export interface Props {
variant?: 'default' | 'hover' | 'glow' | 'bordered';
class?: string;
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
as?: 'div' | 'article' | 'section' | 'a';
href?: string;
}
const {
variant = 'default',
class: className = '',
padding = 'md',
as: Element = 'div',
href
} = Astro.props;
const paddingStyles = {
none: '',
sm: 'p-3 sm:p-4',
md: 'p-4 sm:p-6',
lg: 'p-6 sm:p-8',
xl: 'p-8 sm:p-10'
};
const variantStyles = {
default: 'bg-[var(--color-background-card)]',
hover: 'bg-[var(--color-background-card)] hover:bg-[var(--color-background-card-hover)] hover:shadow-xl hover:scale-[1.02] transition-all duration-300',
glow: 'bg-[var(--color-background-card)] hover:shadow-[0_0_30px_var(--color-primary-glow,rgba(59,130,246,0.3))] transition-all duration-300',
bordered: 'bg-[var(--color-background-card)] border border-[var(--color-border)] hover:border-[var(--color-primary)] transition-colors duration-200'
};
const baseStyles = 'rounded-xl shadow-lg';
const finalClassName = `${baseStyles} ${variantStyles[variant]} ${paddingStyles[padding]} ${className}`;
---
{Element === 'a' ? (
<a href={href} class={finalClassName}>
<slot />
</a>
) : (
<Element class={finalClassName}>
<slot />
</Element>
)}

View file

@ -0,0 +1,30 @@
---
/**
* Shared Container component for consistent max-width and padding
*/
interface Props {
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
class?: string;
as?: 'div' | 'section' | 'article' | 'main';
}
const {
size = 'lg',
class: className = '',
as: Element = 'div'
} = Astro.props;
const sizes = {
sm: 'max-w-3xl',
md: 'max-w-5xl',
lg: 'max-w-7xl',
xl: 'max-w-[1400px]',
full: 'max-w-full'
};
const styles = `mx-auto px-4 sm:px-6 lg:px-8 ${sizes[size]} ${className}`;
---
<Element class={styles}>
<slot />
</Element>

View file

@ -0,0 +1,49 @@
---
/**
* Shared Section Header component for consistent section titles
*/
interface Props {
title: string;
subtitle?: string;
align?: 'left' | 'center' | 'right';
class?: string;
titleClass?: string;
subtitleClass?: string;
}
const {
title,
subtitle,
align = 'center',
class: className = '',
titleClass = '',
subtitleClass = ''
} = Astro.props;
const alignStyles = {
left: 'text-left',
center: 'text-center mx-auto',
right: 'text-right'
};
const containerStyles = `mb-8 md:mb-12 ${alignStyles[align]} ${className}`;
---
<div class={containerStyles}>
<h2 class:list={[
"text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-3 md:mb-4 leading-tight",
titleClass
]}>
<slot name="title">{title}</slot>
</h2>
{subtitle && (
<p class:list={[
"text-base sm:text-lg text-[var(--color-text-secondary)] max-w-3xl leading-relaxed",
align === 'center' && 'mx-auto',
subtitleClass
]}>
<slot name="subtitle">{subtitle}</slot>
</p>
)}
<slot />
</div>

View file

@ -0,0 +1,20 @@
/**
* @manacore/shared-landing-ui
*
* Shared Astro components for landing pages across the Manacore monorepo.
*
* Usage:
* Import components directly from their paths:
*
* ```astro
* ---
* import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
* import HeroSection from '@manacore/shared-landing-ui/sections/HeroSection.astro';
* ---
* ```
*
* Components require CSS custom properties to be defined in your project.
* See utils/index.ts for the required variables and example themes.
*/
export * from './utils/index';

View file

@ -0,0 +1,169 @@
---
/**
* Shared Footer component
* Highly configurable footer for landing pages
*/
import Container from '../atoms/Container.astro';
import Button from '../atoms/Button.astro';
interface FooterLink {
label: string;
href: string;
icon?: string;
external?: boolean;
}
interface FooterSection {
title: string;
links: FooterLink[];
}
interface SocialLink {
platform: 'linkedin' | 'twitter' | 'facebook' | 'instagram' | 'youtube' | 'github';
href: string;
}
interface Props {
brand: {
name: string;
tagline?: string;
logo?: string;
};
sections?: FooterSection[];
socialLinks?: SocialLink[];
copyright?: string;
ctaButton?: {
text: string;
href: string;
variant?: 'primary' | 'secondary';
};
secondaryButton?: {
text: string;
href: string;
};
class?: string;
lang?: string;
}
const {
brand,
sections = [],
socialLinks = [],
copyright,
ctaButton,
secondaryButton,
class: className = '',
lang = 'de'
} = Astro.props;
const currentYear = new Date().getFullYear();
const socialIcons = {
linkedin: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>`,
twitter: `<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>`,
facebook: `<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>`,
instagram: `<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>`,
youtube: `<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>`,
github: `<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>`
};
const socialHoverColors = {
linkedin: 'hover:text-[#0077b5]',
twitter: 'hover:text-white',
facebook: 'hover:text-[#1877f2]',
instagram: 'hover:text-[#E4405F]',
youtube: 'hover:text-[#FF0000]',
github: 'hover:text-white'
};
---
<footer class:list={["bg-[var(--color-background-page)] py-12 md:py-16", className]}>
<Container>
{/* Main Footer Grid */}
{sections.length > 0 && (
<div class:list={[
"grid gap-8 mb-12",
sections.length === 2 && "grid-cols-2 md:grid-cols-4",
sections.length === 3 && "grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
sections.length >= 4 && "grid-cols-2 md:grid-cols-4 lg:grid-cols-5"
]}>
{sections.map((section) => (
<div class="flex flex-col">
<h3 class="text-xs sm:text-sm font-medium text-[var(--color-text-muted)] uppercase tracking-wider mb-4">
{section.title}
</h3>
<ul class="space-y-2">
{section.links.map((link) => (
<li>
<a
href={link.href}
class="flex items-center gap-2 px-3 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-sm transition-all text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
target={link.external ? "_blank" : undefined}
rel={link.external ? "noopener noreferrer" : undefined}
>
{link.icon && <span class="w-4 h-4">{link.icon}</span>}
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
)}
{/* Social Links */}
{socialLinks.length > 0 && (
<div class="border-t border-[var(--color-border)] pt-8 mb-8">
<div class="flex justify-center space-x-6">
{socialLinks.map((social) => (
<a
href={social.href}
target="_blank"
rel="noopener noreferrer"
class:list={[
"text-[var(--color-text-muted)] transition-colors",
socialHoverColors[social.platform]
]}
>
<span class="sr-only">{social.platform}</span>
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<Fragment set:html={socialIcons[social.platform]} />
</svg>
</a>
))}
</div>
</div>
)}
{/* CTA Buttons */}
{(ctaButton || secondaryButton) && (
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-8">
{secondaryButton && (
<Button href={secondaryButton.href} variant="secondary" size="md">
{secondaryButton.text}
</Button>
)}
{ctaButton && (
<Button href={ctaButton.href} variant={ctaButton.variant ?? 'primary'} size="md">
{ctaButton.text}
</Button>
)}
</div>
)}
{/* Copyright */}
<div class="text-center">
<p class="text-[var(--color-text-muted)] text-xs sm:text-sm">
{copyright || `© ${currentYear} ${brand.name}. All rights reserved.`}
</p>
{brand.tagline && (
<p class="text-[var(--color-text-muted)] text-xs mt-1">
{brand.tagline}
</p>
)}
</div>
<slot />
</Container>
</footer>

View file

@ -0,0 +1,74 @@
---
/**
* Shared CTA (Call to Action) Section component
*/
import Container from '../atoms/Container.astro';
import Button from '../atoms/Button.astro';
interface CTA {
text: string;
href: string;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
}
interface Props {
title: string;
subtitle?: string;
primaryCta?: CTA;
secondaryCta?: CTA;
variant?: 'default' | 'highlighted' | 'minimal';
class?: string;
id?: string;
}
const {
title,
subtitle,
primaryCta,
secondaryCta,
variant = 'default',
class: className = '',
id
} = Astro.props;
const variantStyles = {
default: 'bg-[var(--color-background-page)]',
highlighted: 'bg-[var(--color-primary)]/10 border-y border-[var(--color-primary)]/20',
minimal: ''
};
---
<section id={id} class:list={["py-16 md:py-24", variantStyles[variant], className]}>
<Container size="md">
<div class="text-center space-y-6">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] leading-tight">
<slot name="title">{title}</slot>
</h2>
{subtitle && (
<p class="text-base sm:text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto leading-relaxed">
{subtitle}
</p>
)}
{(primaryCta || secondaryCta) && (
<div class="flex flex-col sm:flex-row gap-4 justify-center pt-4">
{primaryCta && (
<Button href={primaryCta.href} variant={primaryCta.variant ?? 'primary'} size="lg">
<slot name="primaryCtaIcon" />
{primaryCta.text}
</Button>
)}
{secondaryCta && (
<Button href={secondaryCta.href} variant={secondaryCta.variant ?? 'secondary'} size="lg">
<slot name="secondaryCtaIcon" />
{secondaryCta.text}
</Button>
)}
</div>
)}
<slot />
</div>
</Container>
</section>

View file

@ -0,0 +1,115 @@
---
/**
* Shared FAQ Section component
* Expandable FAQ items with accordion behavior
*/
import Container from '../atoms/Container.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface FAQItem {
question: string;
answer: string;
}
interface Props {
title?: string;
subtitle?: string;
faqs: FAQItem[];
class?: string;
id?: string;
singleExpand?: boolean;
}
const {
title = 'Frequently Asked Questions',
subtitle,
faqs,
class: className = '',
id = 'faq',
singleExpand = true
} = Astro.props;
const sectionId = `faq-section-${Math.random().toString(36).substring(7)}`;
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container size="md">
<SectionHeader title={title} subtitle={subtitle} />
<div class="max-w-3xl mx-auto space-y-4" data-faq-container={singleExpand ? sectionId : undefined}>
{faqs.map((faq, index) => (
<details class="group" data-faq-item={singleExpand ? sectionId : undefined}>
<summary class="cursor-pointer list-none">
<div class="bg-[var(--color-background-card)] rounded-xl p-5 sm:p-6 border border-[var(--color-border)] hover:bg-[var(--color-background-card-hover)] hover:border-[var(--color-border-hover,var(--color-border))] transition-all duration-200">
<div class="flex items-center justify-between gap-4">
<h3 class="font-semibold text-base sm:text-lg text-[var(--color-text-primary)] pr-4 group-open:text-[var(--color-primary)] transition-colors text-left">
{faq.question}
</h3>
<div class="flex-shrink-0 w-5 h-5 text-[var(--color-primary)] transition-transform duration-300 group-open:rotate-180">
<svg 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>
</div>
</div>
<div class="overflow-hidden max-h-0 group-open:max-h-[1000px] transition-all duration-300 ease-in-out">
<div class="pt-4 text-[var(--color-text-secondary)] leading-relaxed text-sm sm:text-base prose prose-sm max-w-none">
<Fragment set:html={faq.answer} />
</div>
</div>
</div>
</summary>
</details>
))}
</div>
<slot name="contact" />
<slot />
</Container>
</section>
<style>
details summary::-webkit-details-marker {
display: none;
}
details[open] summary ~ * {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<script define:vars={{ sectionId, singleExpand }}>
if (singleExpand) {
function setupFAQ() {
const container = document.querySelector(`[data-faq-container="${sectionId}"]`);
if (!container) return;
const items = container.querySelectorAll(`[data-faq-item="${sectionId}"]`);
items.forEach((item) => {
item.addEventListener('toggle', (e) => {
if (e.target.open) {
items.forEach((otherItem) => {
if (otherItem !== e.target && otherItem.open) {
otherItem.open = false;
}
});
}
});
});
}
setupFAQ();
document.addEventListener('astro:page-load', setupFAQ);
}
</script>

View file

@ -0,0 +1,95 @@
---
/**
* Shared Feature Section component
* Displays features in a responsive grid
*/
import Container from '../atoms/Container.astro';
import Card from '../atoms/Card.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface Feature {
icon: string;
title: string;
description: string;
href?: string;
}
interface Props {
title: string;
subtitle?: string;
features: Feature[];
columns?: 2 | 3 | 4;
variant?: 'cards' | 'simple' | 'icons-left';
class?: string;
id?: string;
}
const {
title,
subtitle,
features,
columns = 3,
variant = 'cards',
class: className = '',
id
} = Astro.props;
const gridCols = {
2: 'md:grid-cols-2',
3: 'md:grid-cols-2 lg:grid-cols-3',
4: 'md:grid-cols-2 lg:grid-cols-4'
};
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class:list={["grid gap-6 md:gap-8", gridCols[columns]]}>
{features.map((feature) => (
variant === 'cards' ? (
<Card variant="hover" padding="lg" as={feature.href ? 'a' : 'div'} href={feature.href}>
<div class="flex flex-col items-center text-center h-full">
<div class="text-4xl sm:text-5xl mb-4 transform group-hover:scale-110 transition-transform duration-300">
{feature.icon}
</div>
<h3 class="font-semibold text-lg sm:text-xl text-[var(--color-text-primary)] mb-3">
{feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] text-sm sm:text-base leading-relaxed">
{feature.description}
</p>
</div>
</Card>
) : variant === 'icons-left' ? (
<div class="flex gap-4">
<div class="flex-shrink-0 text-3xl">
{feature.icon}
</div>
<div>
<h3 class="font-semibold text-lg text-[var(--color-text-primary)] mb-2">
{feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
{feature.description}
</p>
</div>
</div>
) : (
<div class="text-center">
<div class="text-4xl mb-4">{feature.icon}</div>
<h3 class="font-semibold text-lg text-[var(--color-text-primary)] mb-2">
{feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
{feature.description}
</p>
</div>
)
))}
</div>
<slot name="highlight" />
<slot />
</Container>
</section>

View file

@ -0,0 +1,246 @@
---
/**
* Shared Hero Section component
* Supports multiple variants: default (split), fullwidth, widescreen
*/
import Container from '../atoms/Container.astro';
import Button from '../atoms/Button.astro';
interface CTA {
text: string;
href: string;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
icon?: string;
}
interface TrustBadge {
icon: string;
text: string;
}
interface Props {
title: string;
subtitle: string;
variant?: 'default' | 'fullwidth' | 'centered';
image?: {
src: string;
alt: string;
position?: 'left' | 'right';
};
primaryCta?: CTA;
secondaryCta?: CTA;
microCopy?: string;
trustBadges?: TrustBadge[];
class?: string;
showScrollIndicator?: boolean;
}
const {
title,
subtitle,
variant = 'default',
image,
primaryCta,
secondaryCta,
microCopy,
trustBadges,
class: className = '',
showScrollIndicator = false
} = Astro.props;
const imagePosition = image?.position ?? 'right';
---
<section class:list={[
"relative overflow-hidden",
variant === 'default' && "py-12 md:py-20",
variant === 'fullwidth' && "py-8 md:py-12",
variant === 'centered' && "py-16 md:py-24 min-h-[80vh] flex items-center",
className
]}>
<Container>
{variant === 'default' && (
<div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center">
<!-- Text Content -->
<div class:list={[
"text-center md:text-left space-y-6",
imagePosition === 'left' ? 'md:order-2' : 'md:order-1'
]}>
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] leading-tight">
<slot name="title">{title}</slot>
</h1>
<p class="text-base sm:text-lg md:text-xl text-[var(--color-text-secondary)] leading-relaxed max-w-xl mx-auto md:mx-0">
{subtitle}
</p>
{(primaryCta || secondaryCta) && (
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center md:justify-start">
{primaryCta && (
<Button href={primaryCta.href} variant={primaryCta.variant ?? 'primary'} size="lg">
<slot name="primaryCtaIcon" />
{primaryCta.text}
</Button>
)}
{secondaryCta && (
<Button href={secondaryCta.href} variant={secondaryCta.variant ?? 'secondary'} size="lg">
<slot name="secondaryCtaIcon" />
{secondaryCta.text}
</Button>
)}
</div>
)}
{microCopy && (
<p class="text-sm text-[var(--color-text-muted)]">{microCopy}</p>
)}
{trustBadges && trustBadges.length > 0 && (
<div class="flex flex-wrap gap-3 justify-center md:justify-start mt-6">
{trustBadges.map((badge) => (
<div class="inline-flex items-center gap-2 px-4 py-2 bg-white/5 rounded-full border border-white/10 backdrop-blur-sm">
<span class="text-base">{badge.icon}</span>
<span class="text-xs font-medium text-[var(--color-text-secondary)]">{badge.text}</span>
</div>
))}
</div>
)}
</div>
<!-- Image -->
{image && (
<div class:list={[
"relative",
imagePosition === 'left' ? 'md:order-1' : 'md:order-2'
]}>
<div class="relative w-full aspect-square md:aspect-[4/3] overflow-hidden rounded-2xl group">
<img
src={image.src}
alt={image.alt}
class="w-full h-full object-cover shadow-2xl transform group-hover:scale-105 transition-transform duration-700"
loading="eager"
fetchpriority="high"
decoding="async"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent"></div>
</div>
<slot name="imageDecoration" />
</div>
)}
</div>
)}
{variant === 'fullwidth' && image && (
<div class="space-y-6">
<!-- Full-width Image -->
<div class="relative w-full aspect-[16/9] md:aspect-[21/9] overflow-hidden rounded-2xl">
<img
src={image.src}
alt={image.alt}
class="absolute inset-0 w-full h-full object-cover"
loading="eager"
fetchpriority="high"
decoding="async"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
<!-- Overlay text on desktop -->
<div class="hidden md:flex absolute inset-0 items-end justify-center pb-8">
<div class="text-center max-w-3xl px-8 space-y-4">
<h1 class="text-3xl md:text-5xl text-white font-bold leading-tight">
<slot name="title">{title}</slot>
</h1>
<p class="text-lg md:text-xl text-gray-200 leading-relaxed">
{subtitle}
</p>
</div>
</div>
</div>
<!-- Text below on mobile -->
<div class="md:hidden text-center space-y-4">
<h1 class="text-2xl sm:text-3xl font-bold text-[var(--color-text-primary)] leading-tight">
<slot name="title">{title}</slot>
</h1>
<p class="text-base text-[var(--color-text-secondary)] leading-relaxed">
{subtitle}
</p>
</div>
<!-- CTAs -->
{(primaryCta || secondaryCta) && (
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center">
{primaryCta && (
<Button href={primaryCta.href} variant={primaryCta.variant ?? 'primary'} size="lg">
<slot name="primaryCtaIcon" />
{primaryCta.text}
</Button>
)}
{secondaryCta && (
<Button href={secondaryCta.href} variant={secondaryCta.variant ?? 'secondary'} size="lg">
<slot name="secondaryCtaIcon" />
{secondaryCta.text}
</Button>
)}
</div>
)}
{trustBadges && trustBadges.length > 0 && (
<div class="flex flex-wrap gap-3 justify-center">
{trustBadges.map((badge) => (
<div class="inline-flex items-center gap-2 px-4 py-2 bg-white/5 rounded-full border border-white/10 backdrop-blur-sm">
<span class="text-base">{badge.icon}</span>
<span class="text-xs font-medium text-gray-300">{badge.text}</span>
</div>
))}
</div>
)}
</div>
)}
{variant === 'centered' && (
<div class="text-center max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] leading-tight">
<slot name="title">{title}</slot>
</h1>
<p class="text-lg md:text-xl text-[var(--color-text-secondary)] leading-relaxed max-w-2xl mx-auto">
{subtitle}
</p>
{(primaryCta || secondaryCta) && (
<div class="flex flex-col sm:flex-row gap-4 justify-center">
{primaryCta && (
<Button href={primaryCta.href} variant={primaryCta.variant ?? 'primary'} size="lg">
<slot name="primaryCtaIcon" />
{primaryCta.text}
</Button>
)}
{secondaryCta && (
<Button href={secondaryCta.href} variant={secondaryCta.variant ?? 'secondary'} size="lg">
<slot name="secondaryCtaIcon" />
{secondaryCta.text}
</Button>
)}
</div>
)}
{microCopy && (
<p class="text-sm text-[var(--color-text-muted)]">{microCopy}</p>
)}
<slot name="content" />
</div>
)}
</Container>
{showScrollIndicator && (
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
<svg class="w-6 h-6 text-[var(--color-text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
)}
<slot name="background" />
</section>

View file

@ -0,0 +1,140 @@
---
/**
* Shared Pricing Section component
*/
import Container from '../atoms/Container.astro';
import Card from '../atoms/Card.astro';
import Button from '../atoms/Button.astro';
import Badge from '../atoms/Badge.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface PricingFeature {
text: string;
included: boolean;
}
interface PricingPlan {
name: string;
description?: string;
price: string;
period?: string;
features: PricingFeature[] | string[];
cta: {
text: string;
href: string;
};
highlighted?: boolean;
badge?: string;
}
interface Props {
title: string;
subtitle?: string;
plans: PricingPlan[];
class?: string;
id?: string;
}
const {
title,
subtitle,
plans,
class: className = '',
id = 'pricing'
} = Astro.props;
// Normalize features to always have { text, included } format
function normalizeFeatures(features: PricingFeature[] | string[]): PricingFeature[] {
return features.map(f => typeof f === 'string' ? { text: f, included: true } : f);
}
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class:list={[
"grid gap-6 md:gap-8",
plans.length === 2 && "md:grid-cols-2 max-w-4xl mx-auto",
plans.length === 3 && "md:grid-cols-3",
plans.length >= 4 && "md:grid-cols-2 lg:grid-cols-4"
]}>
{plans.map((plan) => (
<Card
variant={plan.highlighted ? 'glow' : 'bordered'}
padding="lg"
class:list={[
"flex flex-col relative",
plan.highlighted && "ring-2 ring-[var(--color-primary)] scale-105"
]}
>
{plan.badge && (
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge variant="primary">{plan.badge}</Badge>
</div>
)}
<div class="text-center mb-6">
<h3 class="text-xl font-bold text-[var(--color-text-primary)] mb-2">
{plan.name}
</h3>
{plan.description && (
<p class="text-sm text-[var(--color-text-secondary)]">
{plan.description}
</p>
)}
</div>
<div class="text-center mb-6">
<span class="text-4xl font-bold text-[var(--color-text-primary)]">
{plan.price}
</span>
{plan.period && (
<span class="text-[var(--color-text-secondary)]">
/{plan.period}
</span>
)}
</div>
<ul class="space-y-3 mb-8 flex-1">
{normalizeFeatures(plan.features).map((feature) => (
<li class="flex items-start gap-3">
<svg
class:list={[
"w-5 h-5 flex-shrink-0 mt-0.5",
feature.included ? "text-green-500" : "text-[var(--color-text-muted)]"
]}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{feature.included ? (
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
) : (
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
)}
</svg>
<span class:list={[
"text-sm",
feature.included ? "text-[var(--color-text-primary)]" : "text-[var(--color-text-muted)] line-through"
]}>
{feature.text}
</span>
</li>
))}
</ul>
<Button
href={plan.cta.href}
variant={plan.highlighted ? 'primary' : 'secondary'}
fullWidth
>
{plan.cta.text}
</Button>
</Card>
))}
</div>
<slot />
</Container>
</section>

View file

@ -0,0 +1,88 @@
---
/**
* Shared Steps/How It Works Section component
* Displays a step-by-step guide with alternating image positions
*/
import Container from '../atoms/Container.astro';
import Card from '../atoms/Card.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface Step {
number: string | number;
title: string;
description: string;
image?: string;
imageAlt?: string;
}
interface Props {
title: string;
subtitle?: string;
steps: Step[];
showImages?: boolean;
alternateLayout?: boolean;
class?: string;
id?: string;
}
const {
title,
subtitle,
steps,
showImages = true,
alternateLayout = true,
class: className = '',
id
} = Astro.props;
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class="space-y-12 md:space-y-20">
{steps.map((step, index) => (
<div class:list={[
"flex flex-col gap-8 lg:gap-12 items-center",
alternateLayout && index % 2 === 0 ? "lg:flex-row" : "lg:flex-row-reverse"
]}>
<!-- Text Content -->
<div class="flex-1 text-center lg:text-left">
<div class="flex items-center gap-4 mb-4 justify-center lg:justify-start">
<div class="w-12 h-12 md:w-14 md:h-14 bg-[var(--color-primary)] rounded-full flex items-center justify-center shadow-lg">
<span class="text-white font-bold text-xl md:text-2xl">{step.number}</span>
</div>
<h3 class="font-bold text-xl md:text-2xl text-[var(--color-text-primary)]">
{step.title}
</h3>
</div>
<p class="text-[var(--color-text-secondary)] text-base md:text-lg leading-relaxed max-w-xl mx-auto lg:mx-0">
{step.description}
</p>
</div>
<!-- Image -->
{showImages && step.image && (
<div class="flex-1 w-full max-w-md lg:max-w-none">
<Card padding="none" class="overflow-hidden">
<div class="relative aspect-video bg-[var(--color-background-card-hover)]">
<img
src={step.image}
alt={step.imageAlt || step.title}
class="w-full h-full object-cover"
loading="lazy"
decoding="async"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
</div>
</Card>
</div>
)}
</div>
))}
</div>
<slot name="cta" />
<slot />
</Container>
</section>

View file

@ -0,0 +1,121 @@
---
/**
* Shared Testimonial Section component
* Displays customer testimonials in a grid
*/
import Container from '../atoms/Container.astro';
import Card from '../atoms/Card.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
interface Testimonial {
name: string;
role?: string;
company?: string;
text: string;
image?: string;
rating?: number;
}
interface Props {
title: string;
subtitle?: string;
testimonials: Testimonial[];
columns?: 1 | 2 | 3;
showRating?: boolean;
class?: string;
id?: string;
}
const {
title,
subtitle,
testimonials,
columns = 3,
showRating = true,
class: className = '',
id
} = Astro.props;
const gridCols = {
1: 'max-w-2xl mx-auto',
2: 'md:grid-cols-2 max-w-4xl mx-auto',
3: 'md:grid-cols-2 lg:grid-cols-3'
};
---
<section id={id} class:list={["py-16 md:py-24", className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class:list={["grid gap-6 md:gap-8", gridCols[columns]]}>
{testimonials.map((testimonial) => (
<Card variant="bordered" padding="lg">
<div class="flex flex-col h-full">
<!-- Header with avatar and info -->
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0">
{testimonial.image ? (
<img
src={testimonial.image}
alt={testimonial.name}
class="w-12 h-12 rounded-full object-cover border-2 border-[var(--color-primary)]/20"
width="48"
height="48"
loading="lazy"
decoding="async"
/>
) : (
<div class="w-12 h-12 rounded-full bg-[var(--color-primary)]/20 flex items-center justify-center">
<span class="text-xl font-semibold text-[var(--color-primary)]">
{testimonial.name.charAt(0)}
</span>
</div>
)}
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-[var(--color-text-primary)] truncate">
{testimonial.name}
</h3>
{testimonial.role && (
<p class="text-sm text-[var(--color-text-secondary)] truncate">
{testimonial.role}
</p>
)}
{testimonial.company && (
<p class="text-sm text-[var(--color-text-muted)] truncate">
{testimonial.company}
</p>
)}
</div>
</div>
<!-- Quote -->
<blockquote class="flex-1 text-[var(--color-text-secondary)] italic mb-4 leading-relaxed">
"{testimonial.text}"
</blockquote>
<!-- Rating -->
{showRating && testimonial.rating && (
<div class="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<svg
class:list={[
"w-5 h-5",
i < testimonial.rating ? "text-[var(--color-primary)]" : "text-[var(--color-border)]"
]}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
)}
</div>
</Card>
))}
</div>
<slot />
</Container>
</section>

View file

@ -0,0 +1,81 @@
/**
* Default Theme - Neutral Dark Theme
* This is a generic dark theme that works well for any app
* Import this file or one of the app-specific themes
*/
:root {
/* Primary colors - Neutral Blue */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-glow: rgba(59, 130, 246, 0.3);
/* Text colors */
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #111827;
--color-background-card: #1f2937;
--color-background-card-hover: #374151;
/* Border colors */
--color-border: #374151;
--color-border-hover: #4b5563;
}
/* Light mode support */
@media (prefers-color-scheme: light) {
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary-glow: rgba(37, 99, 235, 0.3);
--color-text-primary: #111827;
--color-text-secondary: #4b5563;
--color-text-muted: #9ca3af;
--color-background-page: #ffffff;
--color-background-card: #f9fafb;
--color-background-card-hover: #f3f4f6;
--color-border: #e5e7eb;
--color-border-hover: #d1d5db;
}
}
/* Force dark mode class */
.dark {
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-primary-glow: rgba(59, 130, 246, 0.3);
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
--color-background-page: #111827;
--color-background-card: #1f2937;
--color-background-card-hover: #374151;
--color-border: #374151;
--color-border-hover: #4b5563;
}
/* Force light mode class */
.light {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary-glow: rgba(37, 99, 235, 0.3);
--color-text-primary: #111827;
--color-text-secondary: #4b5563;
--color-text-muted: #9ca3af;
--color-background-page: #ffffff;
--color-background-card: #f9fafb;
--color-background-card-hover: #f3f4f6;
--color-border: #e5e7eb;
--color-border-hover: #d1d5db;
}

View file

@ -0,0 +1,24 @@
/**
* Märchenzauber Theme - Golden/Yellow Dark Theme (Children's App)
* Import this file in your landing page to use the Märchenzauber color scheme
*/
:root {
/* Primary colors - Märchenzauber Gold */
--color-primary: #6D5B00;
--color-primary-hover: #F8D62B;
--color-primary-glow: rgba(248, 214, 43, 0.3);
/* Text colors */
--color-text-primary: #FFFFFF;
--color-text-secondary: #999999;
--color-text-muted: #666666;
/* Background colors */
--color-background-page: #181818;
--color-background-card: #2C2C2C;
--color-background-card-hover: #333333;
/* Border colors */
--color-border: #444444;
--color-border-hover: #555555;
}

View file

@ -0,0 +1,24 @@
/**
* ManaCore Theme - Blue Dark Theme
* Import this file in your landing page to use the ManaCore color scheme
*/
:root {
/* Primary colors - ManaCore Blue */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-glow: rgba(59, 130, 246, 0.3);
/* Text colors */
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #081320;
--color-background-card: #1e293b;
--color-background-card-hover: #334155;
/* Border colors */
--color-border: #334155;
--color-border-hover: #475569;
}

View file

@ -0,0 +1,24 @@
/**
* ManaDeck Theme - Purple Dark Theme
* Import this file in your landing page to use the ManaDeck color scheme
*/
:root {
/* Primary colors - ManaDeck Purple */
--color-primary: #7C3AED;
--color-primary-hover: #8B5CF6;
--color-primary-glow: rgba(124, 58, 237, 0.3);
/* Text colors */
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #0f0a1a;
--color-background-card: #1a1625;
--color-background-card-hover: #2d2640;
/* Border colors */
--color-border: #3d3555;
--color-border-hover: #4d4570;
}

View file

@ -0,0 +1,24 @@
/**
* Memoro Theme - Yellow/Gold Dark Theme
* Import this file in your landing page to use the Memoro color scheme
*/
:root {
/* Primary colors - Memoro Yellow/Gold */
--color-primary: #806b00;
--color-primary-hover: #ae9200;
--color-primary-glow: rgba(248, 214, 43, 0.3);
/* Text colors */
--color-text-primary: #ffffff;
--color-text-secondary: #9ca3af;
--color-text-muted: #6b7280;
/* Background colors */
--color-background-page: #0D0C12;
--color-background-card: #1a1a1a;
--color-background-card-hover: #262626;
/* Border colors */
--color-border: #333333;
--color-border-hover: #404040;
}

View file

@ -0,0 +1,106 @@
/**
* Utility functions for shared landing UI components
*/
/**
* CSS custom properties that should be defined in your project's CSS
* for the shared components to work correctly.
*/
export const requiredCssVariables = [
'--color-primary',
'--color-primary-hover',
'--color-primary-glow',
'--color-text-primary',
'--color-text-secondary',
'--color-text-muted',
'--color-background-page',
'--color-background-card',
'--color-background-card-hover',
'--color-border',
'--color-border-hover',
] as const;
/**
* Example CSS variable definitions for light theme
*/
export const exampleLightTheme = `
:root {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-glow: rgba(59, 130, 246, 0.3);
--color-text-primary: #111827;
--color-text-secondary: #4b5563;
--color-text-muted: #9ca3af;
--color-background-page: #ffffff;
--color-background-card: #f9fafb;
--color-background-card-hover: #f3f4f6;
--color-border: #e5e7eb;
--color-border-hover: #d1d5db;
}
`;
/**
* Example CSS variable definitions for dark theme
*/
export const exampleDarkTheme = `
:root {
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-primary-glow: rgba(59, 130, 246, 0.3);
--color-text-primary: #f9fafb;
--color-text-secondary: #d1d5db;
--color-text-muted: #6b7280;
--color-background-page: #111827;
--color-background-card: #1f2937;
--color-background-card-hover: #374151;
--color-border: #374151;
--color-border-hover: #4b5563;
}
`;
/**
* Type definitions for component props (for TypeScript users)
*/
export interface ButtonProps {
href?: string;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
}
export interface CardProps {
variant?: 'default' | 'hover' | 'glow' | 'bordered';
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
}
export interface Feature {
icon: string;
title: string;
description: string;
href?: string;
}
export interface Testimonial {
name: string;
role?: string;
company?: string;
text: string;
image?: string;
rating?: number;
}
export interface FAQItem {
question: string;
answer: string;
}
export interface PricingPlan {
name: string;
description?: string;
price: string;
period?: string;
features: Array<{ text: string; included: boolean } | string>;
cta: { text: string; href: string };
highlighted?: boolean;
badge?: string;
}

View file

@ -0,0 +1,13 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strict": true,
"declaration": true,
"declarationMap": true,
"moduleResolution": "bundler",
"target": "ES2022",
"module": "ES2022"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}