Initial commit

This commit is contained in:
2026-05-05 11:48:58 +03:00
commit 01f34cbb1e
265 changed files with 32550 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
---
paths:
- "src/components/**/*.tsx"
---
# Accessibility Patterns
## Interactive Components
### ariaLabel Prop
Always include `ariaLabel` prop with sensible fallback:
```typescript
interface Props {
ariaLabel?: string;
}
<section aria-label={ariaLabel || "Default section label"}>
```
### Disabled States
```tsx
<button
disabled={isDisabled}
className="disabled:cursor-not-allowed disabled:opacity-50"
>
```
### Focus Indicators
Custom focus indicators for consistent styling:
```tsx
<button className="focus:outline-none focus:ring-2 focus:ring-foreground/50">
```
---
## Media Components
### Images
Use `aria-hidden` for decorative images:
```tsx
<img
src={imageSrc}
alt={imageAlt}
aria-hidden={imageAlt === ""}
/>
```
### Videos
Always include `videoAriaLabel` prop:
```tsx
<video aria-label={videoAriaLabel || "Video content"}>
```
---
## Semantic HTML
### Heading Hierarchy
- `h1` - Page title (one per page)
- `h2` - Section headings
- `h3` - Subsection headings
- `h4` - Card titles
### Semantic Elements
Use appropriate semantic elements:
- `<section>` - Thematic groupings of content
- `<nav>` - Navigation blocks
- `<article>` - Self-contained content
- `<aside>` - Tangentially related content
- `<main>` - Primary content area
- `<header>` / `<footer>` - Section headers/footers

View File

@@ -0,0 +1,95 @@
---
paths:
- "src/components/text/**/*.tsx"
- "src/components/hooks/**/*.ts"
- "src/components/sections/**/*.tsx"
---
# Animation Patterns
## Button Animation Types
Apply to `tagAnimation` or `buttonAnimation` props:
```typescript
type ButtonAnimationType = "none" | "opacity" | "slide-up" | "blur-reveal";
```
- `none` - No animation
- `opacity` - Fade in
- `slide-up` - Fade + slide from bottom
- `blur-reveal` - Fade with blur clearing
---
## Text Animation Types
Set via `defaultTextAnimation` in ThemeProvider or `type` prop in TextAnimation:
```typescript
type AnimationType = "entrance-slide" | "reveal-blur" | "background-highlight";
```
- `entrance-slide` - Characters slide up from bottom
- `reveal-blur` - Text appears with blur clearing
- `background-highlight` - Text highlights from dim to full opacity
---
## Animation Hooks
```typescript
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
// Animate children on scroll
const { containerRef } = useButtonAnimation({ animationType: "slide-up" });
<div ref={containerRef}>
<button>Animates on scroll</button>
</div>
```
---
## CardStack Animation Types
- `none` - No animation
- `opacity` - Fade in
- `slide-up` - Slide with stagger
- `scale-rotate` - Scale + rotate with stagger
- `blur-reveal` - Blur to clear with stagger
- `depth-3d` - 3D perspective (grid only, desktop)
---
## GSAP Best Practices
### Plugin Registration
Register plugins at file level, not in useEffect:
```typescript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
```
### Cleanup with gsap.context()
Always use gsap.context() for proper cleanup:
```typescript
useEffect(() => {
const ctx = gsap.context(() => {
gsap.to(".element", { opacity: 1 });
}, containerRef);
return () => ctx.revert();
}, []);
```
### Performance
- Use `force3D: true` for GPU acceleration
- Prefer transform properties (x, y, scale, rotation)
- Consider disabling complex animations on mobile

159
.claude/rules/components.md Normal file
View File

@@ -0,0 +1,159 @@
---
paths:
- "src/components/**/*.tsx"
---
# Component Patterns
## Naming Conventions (CRITICAL)
### Prop Naming
| Use | Don't Use |
|-----|-----------|
| `title` | `heading`, `headline` |
| `description` | `subtitle`, `text`, `content` |
| `text` (for buttons) | `title`, `label`, `buttonText` |
### className Prop Pattern
- `className` - Main wrapper element
- `containerClassName` - Inner container
- `[element]ClassName` - Specific elements (titleClassName, imageClassName)
### ButtonConfig
```typescript
// CORRECT - variant controlled by ThemeProvider
{ text: "Get Started", href: "/start" }
// WRONG - never specify variant in config
{ text: "Get Started", variant: "primary" } // ❌
```
---
## Component Composition Principle (CRITICAL)
When creating new components, **ALWAYS base them on existing similar components** to maintain consistency.
### The Rule
1. **Identify the base component** - Find an existing component with similar layout/purpose
2. **Identify feature components** - Find existing implementations of any features you're adding
3. **Confirm with user** - Propose which components you'll use as references before implementing
4. **Compose, don't reinvent** - Use patterns, code structure, and styling from the base components
---
## Canonical Base Components
### Hero Sections
| Component | Use As Base When |
|-----------|------------------|
| `HeroSplit` | Split layouts with media left/right positioning |
| `HeroBillboard` | Full-width centered layouts with media below |
| `HeroOverlay` | Media backgrounds with text overlay |
### Feature Sections
| Component | Use As Base When |
|-----------|------------------|
| `FeatureBento` | Complex card layouts with multiple variants |
| `FeatureCardOne` | Simple media + text card grids |
| `FeatureCardSix` | Sequential process/step displays |
### Testimonial Sections
| Component | Use As Base When |
|-----------|------------------|
| `TestimonialCardThirteen` | Card-based testimonials with ratings |
| `TestimonialCardTen` | Testimonials with associated media |
### Other Sections
| Component | Use As Base When |
|-----------|------------------|
| `PricingCardThree` | Comparison tables with feature lists |
| `TeamCardOne` | Image-first cards with overlay text |
| `ContactSplitForm` | Forms with media split layout |
| `FaqDouble` | Two-column accordion layouts |
| `FooterBase` | Simple column-based footers |
| `MetricCardOne` | Large number displays with gradient effects |
| `BlogCardOne` | Content cards with metadata and images |
| `SplitAbout` | Split layouts with bullet points |
---
## Canonical Shared Components
Always use these existing implementations rather than creating new ones:
| Component | Purpose | File |
|-----------|---------|------|
| `MediaContent` | Image/video display | `/src/components/shared/MediaContent.tsx` |
| `AvatarGroup` | Avatar displays with overflow | `/src/components/shared/AvatarGroup.tsx` |
| `TestimonialAuthor` | Author/attribution cards | `/src/components/shared/TestimonialAuthor.tsx` |
| `LogoMarquee` | Scrolling logo displays | `/src/components/shared/LogoMarquee.tsx` |
| `Tag` | Theme-aware badges | `/src/components/shared/Tag.tsx` |
| `Badge` | Simple primary badges | `/src/components/shared/Badge.tsx` |
| `CardStack` | Grid/carousel layouts | `/src/components/cardStack/CardStack.tsx` |
| `CardStackTextBox` | Section headers | `/src/components/cardStack/CardStackTextBox.tsx` |
| `TextAnimation` | Animated text | `/src/components/text/TextAnimation.tsx` |
---
## Core Component Usage
### TextBox
```typescript
import TextBox from "@/components/Textbox";
// Default layout - centered stack
<TextBox
title="Your Title"
description="Your description text"
tag="Optional Tag"
tagIcon={Sparkles}
buttons={[{ text: "Primary", href: "/action" }]}
center={true}
/>
// Layout options: "default" | "split" | "split-actions" | "split-description" | "inline-image"
```
### Button
Buttons automatically get styling based on their index:
- **Index 0** = `primary-button` class with `text-primary-cta-text`
- **Index 1+** = `secondary-button` class with `text-secondary-cta-text`
### MediaContent
```typescript
import MediaContent from "@/components/shared/MediaContent";
<MediaContent
imageSrc="/image.jpg"
imageAlt="Description"
imageClassName="rounded-theme"
/>
// Video takes precedence over image when both provided
<MediaContent
videoSrc="/video.mp4"
videoAriaLabel="Video description"
/>
```
### AnimationContainer
```typescript
import AnimationContainer from "@/components/sections/AnimationContainer";
<AnimationContainer>
<div>Content animates in (fade + slide)</div>
</AnimationContainer>
<AnimationContainer animationType="fade">
<div>Fades in only</div>
</AnimationContainer>
```

View File

@@ -0,0 +1,182 @@
# Inline-Friendly Section Conventions (Option A)
Active rewrite: every section in `src/components/sections/**` becomes a *flat-bodied* React component whose JSX can be lifted verbatim into a generated `HomePage.tsx` by the snippet AST inliner. **Only the file body changes** — the props interface stays exactly as it is so the snippet pipeline keeps working.
These rules SUPERSEDE `components.md` / `transformation.md` / `animations.md` for files in `src/components/sections/**`. The legacy docs still describe how the v4 *showcase* and shared helpers are wired, but a section file written today must not import any of those helpers.
---
## What "inline-friendly" means
After the AST inliner expands `<HeroSplit ... />` into HomePage, the resulting JSX must be:
1. **Flat** — only browser-intrinsic tags (`section`, `div`, `h1`, `p`, `a`, `img`, `video`, `button`, `form`, `input`, `nav`, etc.), `motion.<tag>` from `motion/react`, lucide icons, and the five whitelisted open-source primitives.
2. **Self-explanatory to an LLM** — class names from the v4 fluid scale (`text-7xl`, `w-content-width`, `p-1``p-8`, `gap-1``gap-8`) and shadcn aliases (`bg-primary`, `text-muted-foreground`, `border-border`) only.
3. **Identifier-clean** — every name referenced inside the JSX is either a destructured prop, a `const` declared at the top of the function, or imported from one of the allowed sources below.
If any one of those three is broken, free-edit will misfire. No exceptions.
---
## Allowed imports — exhaustive list
```ts
// Animation
import { motion } from "motion/react";
// Icons
import { Star, ArrowRight /* ... any lucide icon */ } from "lucide-react";
// Open-source primitives (curated five)
import TextAnimation from "@/components/ui/text-animation";
import HoverPattern from "@/components/ui/hover-pattern";
import ShimmerText from "@/components/ui/shimmer-text";
import Marquee from "@/components/ui/marquee";
import GlowCard from "@/components/ui/glow-card";
```
Anything else is forbidden, including (non-exhaustive):
- `@/components/ui/Button`, `@/components/ui/TextAnimation` (capital T — the closed v4 one), `@/components/ui/ScrollReveal`, `@/components/ui/ImageOrVideo`, `@/components/ui/HeroBackgroundSlot`, `@/components/ui/GridOrCarousel`, any `@/components/ui/*Background*`, any `@/components/ui/Navbar*`, any `@/components/ui/Button*`, `Card`, `Carousel`, `Modal`, `Sheet`, `Accordion`, `Tooltip`, `Dropdown`, `DropdownMenu`, `Tag`, `Tabs`, `Spinner`, `Switch`, `Checkbox`, `Input`, `Label`, `Textarea`, `MediaStack`, `OrbitingIcons`, `LoopCarousel`, `TiltedCarousel`, `TiltedStackCards`, `AnimatedBarChart`, `AvatarGroup`, `BorderGlow`, `AutoFillText`, `IconTextMarquee`, `ChatMarquee`, `InfoCardMarquee`, `ChecklistTimeline`, `Calendar`, `Separator`, `TextLink`, `Transition`, `StyleProvider`, `useStyle`.
- `@/components/shared/*` — every shared helper.
- `@/components/cardStack/*`.
- `@/components/text/*`.
- `@/hooks/*` — sections must be stateless; if interactivity is needed, ship a tiny inline `useState` BUT only inside one of the five open-source primitives, never at section level.
- `gsap`, `framer-motion` (use `motion/react`), `react-router-dom`, anything else that isn't already in `webild-vite` `package.json`.
---
## Replacement table
| Closed v4 primitive | Inline replacement |
|---|---|
| `<Button text="Get started" href="/x" variant="primary" />` | `<a href="/x" className="primary-button rounded-theme px-6 h-9 inline-flex items-center justify-center text-primary-cta-text text-sm">Get started</a>` |
| `<Button ... variant="secondary" />` | same shape with `secondary-button` and `text-secondary-cta-text` |
| `<TextAnimation text={title} tag="h1" variant="fade" gradientText className="text-7xl" />` (closed v4) | `<motion.h1 initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-15%" }} transition={{ duration: 0.6, ease: "easeOut" }} className="text-7xl font-medium text-balance">{title}</motion.h1>` |
| `<ScrollReveal variant="slide-up" delay={0.2}>...</ScrollReveal>` | `<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10%" }} transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}>...</motion.div>` |
| `<ImageOrVideo imageSrc={src} />` | `<img src={imageSrc} alt={imageAlt ?? ""} className="w-full h-full object-cover" />` (drop the conditional video branch unless the section's prop type actually has `videoSrc`; if it does, render a ternary inline) |
| `<HeroBackgroundSlot />` | omit (background is delivered by the wrapper `<Layout>` or by inline gradients on the section itself) |
| `<GridOrCarousel items={...}>` | inline `<div className="grid grid-cols-1 md:grid-cols-3 gap-6">` and `.map(item => ...)` directly. No carousel — generated sites don't need touch carousels. If the original section had a true mobile carousel, replace with a horizontal scroll snap container. |
| `<AvatarGroup avatars={...} />` | inline `<div className="flex -space-x-2">{avatars.map(...)}</div>` |
| `<Tag text="..." />` | `<span className="card rounded-full px-3 py-1 text-sm">{text}</span>` |
| `<MediaContent imageSrc={...} />` | inline `<img>` (and `<video>` when applicable) |
For the **animated text effect** (used to be `TextAnimation` with `variant="fade"` / `gradientText`), use the new open-source `@/components/ui/text-animation` primitive: it supports per-word stagger but receives the text string as a child prop, so an LLM can edit the text by changing one literal.
---
## Required structure
Every section file must conform to this skeleton:
```tsx
import { motion } from "motion/react";
// ...allowed icons + primitives only
type FooSectionProps = {
// props identical to the existing v4 prop type — DO NOT change names or shapes.
// The snippet AST inliner reads these to substitute literals.
};
const FooSection = ({ title, description, items, ... }: FooSectionProps) => {
return (
<section
data-webild-section="FooSection"
aria-label="Foo section"
className="relative w-full py-hero-page-padding"
>
<div className="w-content-width mx-auto">
{/* flat JSX only */}
</div>
</section>
);
};
export default FooSection;
```
Hard requirements:
- `data-webild-section="<ComponentName>"` on the outermost `<section>` — used by the selection bridge (Alt+click → postMessage).
- Outermost `<section>` carries `aria-label`. Nested `<section>` blocks are forbidden inside a single section file.
- Default export is the component, name matches file name (`HeroSplit.tsx``export default HeroSplit`).
- Props interface name = `<Component>Props`.
- No `useState`, `useEffect`, `useRef`, `useReducer`, `useCallback`, `useMemo` at section level — sections are stateless. If the original section used a hook (`useEmblaCarousel`, `useScrollProgress`, `useTransform`), drop the dynamic behavior and replace with a static layout.
- No `"use client"` directive — Vite + React is single-mode.
---
## Class vocabulary — what to use
The v4 fluid scale is the source of truth and is mapped through tailwind `@theme inline`. Both showcase and sandbox define the exact same variables, so any class below renders identically in both environments.
### Layout
- Container: `w-content-width mx-auto` (or `max-w-content mx-auto` — same thing via alias)
- Page padding: `py-hero-page-padding` (top of hero), `py-16 md:py-24` (other sections — these resolve to fluid via padding scale)
- Section wrapper: `relative w-full`
- Spacing: `gap-1``gap-8`, `p-1``p-8`, `m-1``m-8` only. Do **not** invent `gap-12` or `p-10`.
### Typography
- `text-base` / `text-lg` body
- `text-2xl``text-4xl` for sub-headings
- `text-6xl``text-8xl` for hero / section headings
- `font-medium` for headings, `font-semibold` only when the design clearly calls for it
- `text-balance` on H1, `leading-tight` on long paragraphs
- Color: `text-foreground` (default), `text-muted-foreground` (secondary), `text-primary` (accent), `text-primary-cta-text` / `text-secondary-cta-text` on filled buttons
### Color & surface
- Page bg: omit — `<body>` paints `--background`
- Card bg: `card` utility class (already defined globally — gradient + border + shadow), with `rounded-theme p-4``p-6`
- Inverted bg sections: `bg-foreground text-background`
- Borders: `border border-border`
- Accent surfaces: `bg-primary` / `bg-secondary` / `bg-muted`
### Buttons
- Primary: `<a className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text">Label</a>`
- Secondary: `<a className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text">Label</a>`
- Buttons are always `<a>` (with optional `<button>` for forms). Never `<Button>`.
### Radius
- `rounded-theme` (theme-aware, the new default)
- `rounded-theme-capped` (capped at xl)
- `rounded-full` (pills, avatars)
- Don't use `rounded-2xl`, `rounded-3xl` — they aren't in the fluid scale.
### Animation
- One canonical fade-in motif:
```tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
```
- Stagger via incremental `delay`: heading 0, sub 0.1, button row 0.2, media 0.3.
- Hover: rely on Tailwind `hover:` utilities, no JS.
---
## Snippet pipeline contract
When the snippet AST inliner ingests `<HeroSplit tag="X" title="Y" .../>`, it:
1. Reads `HeroSplit.tsx`.
2. Substitutes destructured prop usages with the literals from the JSX call.
3. Pastes the resulting JSX into `HomePage.tsx`.
4. Hoists imports (deduped).
For step 2 to work, every prop must appear in the JSX exactly as `{propName}` or `{propName.field}` — no spreading (`{...props}`), no renaming (`const t = title`), no helper functions that consume props before render. Compute layout-only locals (e.g. `const isPrimary = i === 0`) is fine; transforming a prop value before render is not.
For step 4 to work, every import must be a top-level static `import { ... } from "..."` declaration. No dynamic `import()`, no conditional re-exports.
---
## Workflow when rewriting a section
1. Open `http://localhost:3000/components/sections/<category>/<name>` in the browser the user has running.
2. Take a fullPage screenshot — that's the visual ground truth. The rewrite must reproduce the same layout, hierarchy, and tone. Faster = better, but never at the cost of visual parity.
3. Read the existing `.tsx` to capture the exact props interface — do not rename or drop props.
4. Replace the body with flat inline JSX following the rules above.
5. Verify with `pnpm typecheck` from `webild-components-version-4/`.
6. Reload the showcase page and compare to the baseline screenshot.

123
.claude/rules/registry.md Normal file
View File

@@ -0,0 +1,123 @@
---
paths:
- "registry/**/*.json"
---
# Registry Patterns
The registry system enables AI tooling and code generation for components.
---
## Registry Files
| File | Purpose | When to Update |
|------|---------|----------------|
| `registry.json` | Full component configs with constraints | New component, prop changes |
| `registry/components/[Name].json` | Individual component config | New component, prop changes |
| `registry/schemas/[Name].schema.json` | Props schema for code gen | New component, prop changes |
| `registry/index.json` | Category/intent mapping | New component |
| `registry/intents.json` | Intent groupings | New component or intent |
---
## registry/index.json Entry
```json
{
"ComponentName": {
"category": "hero",
"intent": "hero with media",
"bestFor": ["landing pages", "product launches"],
"avoidWhen": ["simple text-only sections"],
"requires": ["title", "description", "background"],
"import": "@/components/sections/hero/ComponentName"
}
}
```
---
## registry/components/[Name].json Format
```json
{
"name": "ComponentName",
"description": "Brief description of the component",
"details": "Detailed usage information",
"constraints": {
"textRules": {
"title": {
"required": true,
"example": "Example title text",
"minChars": 4,
"maxChars": 50
},
"description": {
"required": true,
"example": "Example description text",
"minChars": 20,
"maxChars": 200
}
}
},
"propsSchema": {
"title": "string",
"description": "string",
"optionalProp?": "string (default: 'value')"
},
"usageExample": "<ComponentName title=\"...\" />",
"do": ["Use for X", "Use for Y"],
"dont": ["Don't use for Z"],
"editRules": {
"textOnly": true,
"layoutLocked": true,
"styleLocked": true
}
}
```
---
## registry/schemas/[Name].schema.json Format
```json
{
"name": "ComponentName",
"propsSchema": {
"title": "string",
"description": "string",
"optionalProp?": "string (default: 'value')"
}
}
```
---
## Props Schema Conventions
- Required props: `"propName": "type"`
- Optional props: `"propName?": "type (default: 'value')"`
- Arrays: `"items": "Array<{ name: string, value: string }>"`
- Complex types: Include description after type
Example:
```json
{
"title": "string",
"description": "string",
"inputs": "Array<{ name: string, type: string, placeholder: string }> - Form input fields",
"mediaPosition?": "'left' | 'right' (default: 'right')",
"onSubmit?": "(data: Record<string, string>) => void"
}
```
---
## Keeping Registry in Sync
**Critical**: Registry must exactly match component props.
1. Optional props in component → `"prop?"` in schema
2. Default values in component → `(default: 'value')` in schema
3. All prop types must match TypeScript interface

188
.claude/rules/styling.md Normal file
View File

@@ -0,0 +1,188 @@
---
paths:
- "src/**/*.tsx"
- "src/**/*.css"
---
# Styling Patterns
This codebase uses **custom-defined CSS variables** for all sizing, spacing, and dimensions. **DO NOT use default Tailwind classes** that are not defined in this system.
---
## What NOT to Use
These default Tailwind classes are **NOT defined** and will not work:
```tsx
// DO NOT USE - undefined width classes
w-xs, w-sm, w-md, w-lg, w-xl, w-2xl, w-3xl, w-4xl, w-5xl, w-6xl, w-7xl
w-screen, w-min, w-max, w-fit (except these work)
w-96, w-80, w-72, w-64, w-56, w-48 (fixed pixel widths)
// DO NOT USE - undefined height classes
h-screen (use h-svh instead), h-min, h-max
h-96, h-80, h-72, h-64, h-56, h-48 (fixed pixel widths except defined ones)
// DO NOT USE - undefined spacing beyond 8
p-9, p-10, p-11, p-12, p-14, p-16, p-20, p-24, etc.
m-9, m-10, m-11, m-12, m-14, m-16, m-20, m-24, etc.
gap-9, gap-10, gap-11, gap-12, etc.
// DO NOT USE - undefined border radius
rounded-2xl, rounded-3xl (use rounded-theme or rounded-theme-capped)
```
---
## What TO Use
| Category | Use These | Don't Use |
|----------|-----------|-----------|
| **Width** | `w-5` to `w-100`, `w-content-width`, fractions | `w-xs`, `w-md`, `w-lg`, `w-96` |
| **Height** | `h-4` to `h-12`, `h-30`, `h-90` to `h-150`, `h-svh` | `h-screen`, `h-96`, `h-80` |
| **Padding** | `p-1` to `p-8` | `p-9`, `p-10`, `p-12`, `p-16` |
| **Margin** | `m-1` to `m-8` | `m-9`, `m-10`, `m-12`, `m-16` |
| **Gap** | `gap-1` to `gap-8` | `gap-9`, `gap-10`, `gap-12` |
| **Text** | `text-2xs` to `text-9xl` | (all defined) |
| **Radius** | `rounded-theme`, `rounded-theme-capped` | `rounded-2xl`, `rounded-3xl` |
---
## Spacing Scale (1-8 ONLY)
All spacing uses VW-based fluid scaling. **Only values 1-8 are defined:**
- `p-1` to `p-8`, `m-1` to `m-8`, `gap-1` to `gap-8`
- Directional: `px-4`, `py-6`, `mx-2`, `my-4`, etc.
---
## Content Width (CRITICAL FOR SECTIONS)
**`w-content-width`** is the most important width class - use it for all section content containers:
```tsx
// ALWAYS use w-content-width for section containers
<section className="relative w-full">
<div className="w-content-width mx-auto">
{/* Section content goes here */}
</div>
</section>
<div className="w-content-width" /> // Main content width
<div className="w-content-width-expanded" /> // Expanded (for carousels)
```
---
## Width Scale
| Pattern | Values | Notes |
|---------|--------|-------|
| `w-5` to `w-100` | Increments of 5 (5vw to 100vw) | Main width classes |
| `w-7_5`, `w-12_5`, etc. | Increments of 2.5 | Half-step widths |
| `w-carousel-item-3`, `w-carousel-item-4` | Carousel widths | For carousel items |
| `w-full`, `w-1/2`, `w-1/3`, etc. | Standard fractions | Tailwind defaults work |
---
## Height Scale
Heights use standard rem on desktop, but become vw-based on mobile (< 768px).
- **Standard**: `h-4` to `h-12` (1rem to 3rem)
- **Large**: `h-30`, `h-90` to `h-150` (for larger containers)
- **Viewport**: Use `h-svh` instead of `h-screen`
---
## Text Sizes
`text-2xs` to `text-9xl` - all fluid (clamp-based).
Key sizes:
- `text-base` - body text
- `text-6xl` - section headings
- `text-7xl`/`text-8xl` - hero headings
---
## Border Radius
Use theme-aware classes:
- `rounded-theme` - uses ThemeProvider setting
- `rounded-theme-capped` - max xl
---
## Hero Page Padding
Special padding for hero sections that accounts for navbar:
```tsx
<section className="py-hero-page-padding" /> // Standard
<section className="py-hero-page-padding-half" /> // Half
<section className="py-hero-page-padding-1_5" /> // 1.5x
<section className="py-hero-page-padding-double" /> // Double
```
---
## Color Variables
Defined in `src/app/styles/variables.css`:
```css
--background: #f5f4ef /* Page background */
--card: #dad6cd /* Card backgrounds */
--foreground: #2a2928 /* Text color */
--primary-cta: #2a2928 /* Primary button background */
--primary-cta-text: #f5f4ef /* Primary button text */
--secondary-cta: #ecebea /* Secondary button background */
--secondary-cta-text: #2a2928 /* Secondary button text */
--accent: #ffffff /* Accent highlights, glows */
--background-accent: #c6b180 /* Accent variant */
```
Use as Tailwind classes: `bg-background`, `text-foreground`, etc.
---
## Dynamic CSS Classes
These classes are styled based on ThemeProvider configuration:
```tsx
<div className="card rounded-theme p-6">Card content</div>
<button className="primary-button rounded-theme px-6 py-3">Primary</button>
<button className="secondary-button rounded-theme px-6 py-3">Secondary</button>
```
---
## Inverted Background Pattern
For sections that need dark backgrounds:
```typescript
// Required prop - forces explicit choice
useInvertedBackground: boolean
// Usage in component
<section className={cls("w-full", useInvertedBackground && "bg-foreground")}>
<p className={useInvertedBackground ? "text-background" : "text-foreground"}>
Content
</p>
</section>
```
---
## Using CSS Variables Directly
When you need values not available as Tailwind classes:
```tsx
<div className="bottom-[calc(var(--spacing-4)+var(--spacing-4))]" />
<div className="left-[calc(var(--vw-1)*2)]" />
```

View File

@@ -0,0 +1,69 @@
---
paths:
- "src/components/**/*.tsx"
- "src/hooks/**/*.ts"
---
# v2 to v4 Transformation
## Quick Reference
| v2 | v4 |
|----|-----|
| GSAP + ScrollTrigger | `motion/react` with `whileInView` |
| Character-level text hooks | `TextAnimation` component (word-level) |
| `buttons: ButtonConfig[]` | `primaryButton` / `secondaryButton` objects |
| 10+ className props | Single `className` prop |
| CardStack/TextBox wrappers | Direct composition |
| Complex media props | Discriminated union: `{ imageSrc } \| { videoSrc }` |
## Animation Variants
| v2 | v4 |
|----|-----|
| `entrance-slide` | `slide-up` |
| `reveal-blur` | `fade-blur` |
| `background-highlight` | `fade` |
## Allowed Hooks
Only 2 custom hooks exist in v4:
- `useCarouselControls` - Embla carousel state
- `useButtonClick` - Navigation handling
All other animation hooks are replaced by `TextAnimation`, `Button animate`, or inline `motion`.
## Standard Motion Animation
```tsx
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
```
## Props Pattern
```tsx
type SectionProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
```
## Checklist
1. Replace GSAP with `motion/react`
2. Replace animation hooks with `TextAnimation` or `Button animate`
3. Use discriminated unions for media props
4. Use `primaryButton`/`secondaryButton` instead of buttons array
5. Remove all className props except single `className`
6. Replace CardStack with `GridOrCarousel`
7. Replace TextBox with direct `TextAnimation` + `Button` composition
8. Add `aria-label` to sections
9. Use `w-content-width` for containers

View File

@@ -0,0 +1,41 @@
---
name: add-animation
description: Add animation to a component with proper setup and cleanup
disable-model-invocation: true
---
# Add Animation
Add animation to: $ARGUMENTS
## Process
1. **Choose animation library**
- GSAP: Complex timelines, ScrollTrigger, text splitting
- Framer Motion: Simple component animations, gestures, layout
- CSS: Basic transitions, hover states
2. **For GSAP animations**
- Register plugins at file level
- Use `gsap.context()` for cleanup
- Return `() => ctx.revert()` from useEffect
- Use refs for target elements
3. **For Framer Motion**
- Wrap conditionally rendered elements in `AnimatePresence`
- Add `key` prop for AnimatePresence children
- Use `initial`, `animate`, `exit` props
4. **For scroll-triggered animations**
- Set appropriate `start` and `end` points
- Use `toggleActions` for replay behavior
- Consider mobile: disable or simplify
5. **Performance considerations**
- Use transform properties (x, y, scale, rotation)
- Add `force3D: true` for GPU acceleration
- Check if animation should be disabled on mobile
## Output
Show the implemented animation code with proper cleanup.

View File

@@ -0,0 +1,47 @@
---
name: add-to-registry
description: Add a component to the registry system for AI tooling
disable-model-invocation: true
---
# Add to Registry
Add component to registry: $ARGUMENTS
## Process
1. **Identify component props**
- Read the component's TypeScript interface
- Note required vs optional props
- Note default values
2. **Update registry.json**
- Add full entry with constraints, propsSchema, usage example
- Include textRules for text props (min/max chars)
- Include do/dont guidelines
3. **Create component config**
- Create `registry/components/[Name].json`
- Match structure of existing component configs
4. **Create schema file**
- Create `registry/schemas/[Name].schema.json`
- Include name and propsSchema
- Use `?` suffix for optional props
- Include `(default: 'value')` for defaults
5. **Update index.json**
- Add entry with category, intent, bestFor, avoidWhen
- Include requires array and import path
6. **Update intents.json**
- Add component to relevant intent arrays
7. **Verify consistency**
- Props in schema match component interface exactly
- Optional props marked with `?`
- Default values documented
## Output
Report all updated registry files and confirm props match component.

View File

@@ -0,0 +1,61 @@
---
name: code-review
description: Frontend code review checklist for React, TypeScript, and Tailwind
---
# Frontend Code Review
Review recently changed files against this checklist.
## Process
1. **Identify Changed Files**
- Run `git diff --name-only` to find modified files
- Focus on `.tsx`, `.ts` files in `src/`
2. **Review Each File**
### TypeScript
- [ ] No `any` types (use proper types or `unknown`)
- [ ] Interfaces for component props
- [ ] No unused imports or variables
- [ ] Proper error handling (no silent catches)
### React Patterns
- [ ] `"use client"` only when needed (hooks, events, browser APIs)
- [ ] No unnecessary `React.memo`
- [ ] Stable callbacks with `useCallback` when passed as props
- [ ] Cleanup in `useEffect` (event listeners, subscriptions, GSAP)
- [ ] Keys on mapped elements (not index unless static list)
### Component Structure
- [ ] Logic extracted to hooks (if complex)
- [ ] Props interface defined
- [ ] Default export at bottom
### Tailwind/Styling
- [ ] Using `cls()` for conditional classes
- [ ] Responsive classes (mobile-first)
- [ ] No hardcoded colors (use CSS variables)
- [ ] Proper z-index layering
### Performance
- [ ] Images use `next/image` with proper sizing
- [ ] Heavy components use dynamic import if below fold
- [ ] No unnecessary re-renders (check deps arrays)
### Accessibility
- [ ] Interactive elements are focusable
- [ ] Images have alt text
- [ ] Buttons have accessible names
- [ ] `aria-hidden` on decorative elements
3. **Report Findings**
- List issues grouped by severity: Critical, Warning, Suggestion
- Provide specific line numbers and fixes

View File

@@ -0,0 +1,47 @@
---
name: debug
description: Systematic debugging workflow to find and fix issues
disable-model-invocation: true
---
# Debug
Debug issue: $ARGUMENTS
## Process
1. **Understand the problem**
- What is the expected behavior?
- What is the actual behavior?
- When did it start happening?
- Is it reproducible?
2. **Gather information**
- Check browser console for errors
- Check terminal for server errors
- Look at network requests
- Check component props/state
3. **Isolate the cause**
- Find the smallest reproduction
- Identify which component/function is involved
- Check recent changes to that area
- Add console.logs or debugger statements
4. **Form hypothesis**
- Based on evidence, what could cause this?
- List possible causes in order of likelihood
5. **Test and fix**
- Test most likely cause first
- Make minimal change to fix
- Verify fix doesn't break other things
6. **Verify**
- Confirm original issue is resolved
- Test related functionality
- Remove debug statements
## Output
Report findings, root cause, and the fix applied.

View File

@@ -0,0 +1,57 @@
---
name: performance
description: Performance optimization workflow for frontend applications
disable-model-invocation: true
---
# Performance
Optimize: $ARGUMENTS
## Process
1. **Identify the problem**
- What is slow? (initial load, interaction, render)
- Measure current performance
- Set a target improvement
2. **Analyze causes**
- Large bundle size?
- Unnecessary re-renders?
- Expensive calculations?
- Network waterfalls?
- Layout thrashing?
3. **React optimizations**
- Add `memo()` for expensive list items
- Use `useMemo` for costly computations
- Use `useCallback` for stable function refs
- Split components to isolate re-renders
- Lazy load with `dynamic()` or `React.lazy()`
4. **Bundle optimizations**
- Code split large dependencies
- Dynamic imports for heavy features
- Tree shake unused code
- Analyze with bundle analyzer
5. **Render optimizations**
- Virtualize long lists
- Debounce/throttle frequent updates
- Use CSS transforms over layout properties
- Avoid layout shifts
6. **Network optimizations**
- Preload critical resources
- Cache API responses
- Optimize images (WebP, lazy load)
- Reduce request waterfalls
7. **Verify improvement**
- Measure after changes
- Compare to baseline
- Test on slow devices/networks
## Output
Report optimizations applied and measured improvements.

View File

@@ -0,0 +1,55 @@
---
name: refactor
description: Systematic refactoring workflow for safe code improvements
disable-model-invocation: true
---
# Refactor
Refactor: $ARGUMENTS
## Process
1. **Understand current state**
- Read the code to refactor
- Identify what it does and why
- Note all usages/dependencies
- Check for existing tests
2. **Define the goal**
- What problem are we solving?
- Readability? Performance? Maintainability?
- What should NOT change? (behavior, API, **UI**)
3. **Plan changes**
- Break into small, safe steps
- Each step should leave code working
- Identify risk points
4. **Execute incrementally**
- One change at a time
- Verify after each step
- Keep commits atomic
5. **Common refactors**
- Extract function/component
- Rename for clarity
- Simplify conditionals
- Remove duplication
- Split large files
- Move code to better location
6. **Verify**
- Run existing tests
- Test affected functionality
- Check for regressions
- Ensure behavior unchanged
7. **UI Preservation (Critical)**
- Compare CSS classes line by line before consolidating
- Duplicate code may have subtle differences (sizes, colors)
- Never assume - verify before merging branches
## Output
Report changes made, files affected, and verification steps taken.

View File

@@ -0,0 +1,59 @@
---
name: scaffold-component
description: Generate a new React component following project conventions
disable-model-invocation: true
---
# Scaffold Component
Generate a new component at the specified path with proper structure.
## Usage
`/scaffold-component ComponentName path/to/component`
## Process
1. **Parse Arguments**
- First argument: Component name (PascalCase)
- Second argument: Path relative to `src/components/`
2. **Create Component File**
```tsx
"use client";
import { cls } from "@/lib/utils";
interface $NAMEProps {
className?: string;
}
const $NAME = ({ className }: $NAMEProps) => {
return <div className={cls("", className)}>{/* TODO: Implement */}</div>;
};
export default $NAME;
```
3. **Determine if Hook is Needed**
- Ask user if component needs state/effects
- If yes, create hook file at `src/hooks/{domain}/use{Name}.ts`
4. **Determine if Constants are Needed**
- Ask user if component has static text
- If yes, create constants file at `src/constants/{domain}/{name}.ts`
## File Naming
- Component: `ComponentName.tsx` (PascalCase)
- Hook: `useComponentName.ts` (camelCase with use prefix)
- Constants: `componentName.ts` (camelCase)
## Output
Report created files and remind user to:
1. Add necessary imports
2. Implement the component logic
3. Add to parent component/page

4
.env Normal file
View File

@@ -0,0 +1,4 @@
VITE_API_URL=https://dev.api.webild.io
VITE_PROJECT_ID=3df875bd-3de5-46d4-bbca-c040fd6f398b

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# API Configuration
VITE_API_URL=
VITE_PROJECT_ID=

View File

@@ -0,0 +1,46 @@
name: Code Check
on:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: git clone --depth 1 --branch ${{ gitea.ref_name }} ${{ gitea.server_url }}/${{ gitea.repository }}.git . || exit 1
- name: Install dependencies
run: |
if [ -f "vite.config.ts" ]; then
if [ -d "/var/node_modules_cache_v4/node_modules" ]; then
ln -s /var/node_modules_cache_v4/node_modules ./node_modules
elif [ -f "pnpm-lock.yaml" ]; then
npm install -g pnpm --silent
pnpm install --frozen-lockfile
else
npm ci --prefer-offline --no-audit
fi
elif [ -d "/var/node_modules_cache/node_modules" ]; then
ln -s /var/node_modules_cache/node_modules ./node_modules
else
npm ci --prefer-offline --no-audit
fi
timeout-minutes: 5
- name: TypeScript check
run: npm run typecheck 2>&1 | tee build.log
timeout-minutes: 3
- name: ESLint check
run: npm run lint 2>&1 | tee -a build.log
timeout-minutes: 3
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-log
path: build.log
retention-days: 1

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

218
STRUCTURE.md Normal file
View File

@@ -0,0 +1,218 @@
# AI-Friendly Component Library
## The Problem
Current architecture is optimized for human developers (DRY, reusable, flexible) but this makes AI editing fail. AI can't trace through ThemeProvider indirection, 40+ prop interfaces, nested component layers, and custom Tailwind scales. It hallucinates because it can't see the actual output.
## The Solution
Shift from human-optimized to AI-optimized code:
- **Explicit over implicit** - All styles visible in file, no ThemeProvider magic
- **Flat over nested** - Direct components, no wrapper layers to trace through
- **Concrete over abstract** - Actual Tailwind classes, not prop names that map to hidden styles
- **Standard over custom** - Default Tailwind values AI already knows
- **Simple over flexible** - One way to do things, not 11 variants
Components become templates that AI reads, understands, and edits directly.
---
## 1. Backend-Driven Sections
Generated website repository only contains what is used, plus general UI components. Section components are stored on the backend and injected when needed, not bundled in the boilerplate.
**Alternative:** If backend approach takes too long, keep all sections in the boilerplate but with the new simplified structure defined below.
---
## 2. Props & Structure
Remove all styling/className props. AI edits classes directly in the component file.
**Option A - Minimal props:** Keep text, links, assets, arrays, icons as props. Backend passes content, AI edits props in page file.
**Option B - No props:** All content hardcoded in section file. Backend injects entire file with content baked in. AI edits section file directly. Maximum explicitness.
**Flat structure:** Sections use direct components (TextAnimation, Button, MediaContent) not wrapper components like TextBox that nest other components inside.
---
## 3. Framer Motion for Element Animations
Replace GSAP (ScrollTrigger, `gsap.context()`, `ctx.revert()`) with Framer Motion `motion` divs for cards, buttons, tags, and other elements. Use `whileInView` for scroll-triggered animations.
---
## 4. Simplified Tailwind & Single CSS File
Remove all custom Tailwind values (spacing, widths, gaps). Use default Tailwind classes.
**Keep:**
- Content width approach (for page width and carousels)
- Fluid font sizes (AI picks bad font sizes otherwise)
**One globals.css file with:**
- Color variables in `:root`
- `--radius` variable, applied globally via `@layer base`
- Card styles
- Button styles
- Utility classes (masks, animations) - consolidated, fewer variants
- Base styling (body defaults, scrollbar, heading font-family)
AI picks from a reference list of available styles and writes them directly to globals.css.
---
## 5. Remove ThemeProvider
Delete the entire ThemeProvider system. No dynamic style injection.
**Card/button styles:** AI chooses on backend, adds one card style, one primary button style, one secondary button style to globals.css.
**Text animations:** Remove TextBox component. Use simple `TextAnimation` component in section files with `text` and `animationType` props.
**Button:** Single `components/ui/Button.tsx` with `text`, `href`, and `className` (e.g. `primary-button` defined in globals.css).
**Content width:** Fixed value in globals.css.
**Text sizing:** Fixed values in globals.css. Future: AI picks from 3 presets on backend and adds to globals.css.
**Background components:** Remove all (aurora, grid, floatingGradient, etc.).
**Fonts:** Keep current logic.
---
## 6. New Folder Structure
```
src/
├── app/ # Pages
├── components/
│ ├── ui/ # Atomic UI components
│ └── [sections] # Section components flat, not nested (injected from backend)
├── hooks/ # Common hooks
├── lib/ # Utilities
└── styles/
└── globals.css # Single CSS file
```
Remove: `providers/themeProvider/`, multiple CSS files, `button/` variants folder, `text/` folder, `background/` folder.
---
## 7. Atomic UI Components
Add simple, single-purpose components to `components/ui/`:
Button, TextAnimation, MediaContent, Toggle, Dropdown, Accordion, Avatar, Badge, Breadcrumb, Calendar, Chart, Checkbox, Form, Input, Label, Tooltip
Expand as needed.
---
## 8. Simplify Complex Components
**Navbar:** Separate navbar components instead of one with variants. AI chooses which one. Reduce to 4 navbars for now.
**CardStack → GridLayout:** Replace complex CardStack (mode, gridVariant, carouselThreshold, ~40 props) with simple wrapper. Children as items. Single prop: items per row (3 or 4). Under = grid, over = carousel. Carousel mode keeps controls (prev/next, progress bar). No title/description/animation props - sections handle all that directly. Remove timelines, grid configs, auto-carousel variants for now, refactor later.
**TextBox:** Remove completely. Use TextAnimation and Button directly in sections.
---
## 9. Consistent File Patterns
Every section file follows the EXACT same structure. AI sees one section, knows how all work.
**Template:**
```
1. IMPORTS (same order: react, framer-motion, ui components, icons)
2. CONTENT (variables at top if Option B)
3. COMPONENT (predictable JSX order)
4. EXPORT (always at bottom)
```
**JSX order:** tag → title → description → buttons → media (always same sequence)
---
## 10. Short Files, No Sub-components
**Line limits:**
- Sections: under 100 lines
- UI components: under 50 lines
**Key rule:** Don't split into sub-components to achieve this. Sub-components add nesting/indirection. Instead, simplify the section itself.
If a section is too long, it's too complex. Simplify the design, don't extract parts.
---
## 11. Content at Top
If using Option B (no props), put all editable content as variables at the very top of the file.
AI immediately knows: "edit content? check the top."
---
## 12. Remove Complex Components
Remove overly complex components for now. Add back simplified versions later if needed.
---
## 13. CSS Class Organization
**Consistent class order:** layout → spacing → sizing → typography → colors → effects
**Keep className short.** Too many classes = section too complex. Use global CSS (`.card`, `.btn-primary`) for repeated patterns.
**Use `cls()` for dynamic classes.** When state-based styling is needed (e.g., `menuOpen ? "rotate-45" : "rotate-0"`), use the `cls()` utility from `@/lib/utils`.
**One line if short, split by category if long.**
---
## Implementation Roadmap
### Phase 1: Foundation (Delete)
- Delete `providers/themeProvider/` (13 files)
- Delete `components/background/` (27 files)
- Delete `components/Textbox.tsx`
- Delete `components/text/` (GSAP TextAnimation)
- Consolidate all CSS → single `styles/globals.css`
### Phase 2: Core Simplification
- Button → single `ui/Button.tsx` (50 lines max)
- Navbar → 4 separate components, no animation hooks
- TextAnimation → new Framer Motion version
- CardStack → simple GridLayout wrapper
### Phase 3: Section Refactoring
- Remove all className/containerClassName props
- Remove useTheme() calls
- Each section under 100 lines
- Content variables at top (if Option B)
- Flat structure, no nested wrappers
### Phase 4: Organize
- Create `components/ui/` with atomic components
- Move hooks to `hooks/` folder
- Update all imports
---
## Current → Target
| Metric | Current | Target |
|--------|---------|--------|
| Total files | ~270 | ~100 |
| Lines of code | ~25,000 | ~10,000 |
| Props per section | 20-40 | 5-10 |
| CSS files | 37 | 1 |
| Button variants | 11 | 1 |
| Navbar variants | 5 | 4 |
---

257
TEMPLATES.md Normal file
View File

@@ -0,0 +1,257 @@
# Templates
Templates are complete website starting points. Props for content, default values for demo content, inline sections for freedom.
---
## Quick Setup Flow
### 1. Define Sections
List the sections for the template:
```
NavbarFloating, HeroBillboardBrand, AboutTextSplit, ProductMediaCards,
FeaturesBento, TestimonialTrustCard, FaqSplitMedia, ContactCenter, FooterSimpleCard
```
### 2. Create Files
```
src/templates/[name]/
├── page.tsx
└── theme.css
```
### 3. Build page.tsx
**Imports** - UI components only, never sections:
```tsx
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import AutoFillText from "@/components/ui/AutoFillText";
import { Star, ArrowUpRight } from "lucide-react";
import "./theme.css";
```
**Types** - One type per section:
```tsx
type TemplateProps = {
hero: { brand: string; description: string; /* ... */ };
about: { title: string; description: string; /* ... */ };
};
```
**Default Props** - Demo content:
```tsx
const defaultProps: TemplateProps = {
hero: { brand: "Brand", description: "Description text" },
about: { title: "About Us", description: "..." },
};
```
**Sections** - Copy code from base section, add marker:
```tsx
{/* === HERO (base: HeroBillboardBrand) === */}
<section>
{/* Inline code from HeroBillboardBrand.tsx */}
</section>
```
### 4. Build theme.css
Copy full structure from existing template, change only:
- Color values in `:root`
- Card style (`.card`)
- Button styles (`.primary-button`, `.secondary-button`)
Required structure:
```css
/* [Name] - [Theme Description] */
@import "tailwindcss";
@import "../../styles/masks.css";
@import "../../styles/animations.css";
:root {
/* @colorThemes/[lightTheme|darkTheme]/[themeName] */
--background: #...;
--card: #...;
--foreground: #...;
--primary-cta: #...;
--primary-cta-text: #...;
--secondary-cta: #...;
--secondary-cta-text: #...;
--accent: #...;
--background-accent: #...;
/* @layout/border-radius/rounded */
--radius: 1.5rem;
/* @layout/content-width/medium */
--width-content-width: clamp(40rem, 72.5vw, 100rem);
/* ... carousel, typography variables ... */
}
/* Mobile typography */
@media (max-width: 768px) { :root { /* ... */ } }
@theme inline { /* Tailwind mappings */ }
/* Base styles: *, html, body, h1-h6 */
/* WEBILD_CARD_STYLE */
/* @cards/[style-name] */
.card { /* ... */ }
/* WEBILD_PRIMARY_BUTTON */
/* @buttons/primary-button-styles/[style-name] */
.primary-button { /* ... */ }
/* WEBILD_SECONDARY_BUTTON */
/* @buttons/secondary-button-styles/[style-name] */
.secondary-button { /* ... */ }
```
### 5. Add Route
In `src/pages/components/templates/`:
- Create `[Name]TemplatePage.tsx`
- Add to `TemplateListPage.tsx`
---
## Structure
```
src/templates/
├── saas/
│ ├── page.tsx # Props + defaults + inline sections
│ └── theme.css # Colors, buttons, cards
└── restaurant/
├── page.tsx
└── theme.css
```
---
## Template Pattern
```tsx
// UI components (always imported)
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import "./theme.css";
// Types
type SaasTemplateProps = {
hero: {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
features: { /* ... */ };
};
// Default content (demo data)
const defaultProps: SaasTemplateProps = {
hero: {
tag: "SaaS Platform",
title: "Build Modern Web Experiences",
description: "Create stunning websites...",
primaryButton: { text: "Get Started", href: "#contact" },
secondaryButton: { text: "Learn More", href: "#features" },
imageSrc: "https://...",
},
features: { /* ... */ },
};
// Template component
const SaasTemplate = ({
hero = defaultProps.hero,
features = defaultProps.features,
}: Partial<SaasTemplateProps>) => {
return (
<>
{/* === HERO (base: HeroSplit) === */}
<section aria-label="Hero section" className="...">
<TextAnimation text={hero.title} /* ... */ />
<Button text={hero.primaryButton.text} href={hero.primaryButton.href} />
<ImageOrVideo imageSrc={hero.imageSrc} videoSrc={hero.videoSrc} />
</section>
{/* === FEATURES (base: FeaturesMediaCards) === */}
<section aria-label="Features section">
{/* Inline code using features prop */}
</section>
</>
);
};
export default SaasTemplate;
```
---
## Base Markers
Each section has a comment marking its base section:
```tsx
{/* === HERO (base: HeroSplit) === */}
```
Find all templates using a section: `grep -r "base: HeroSplit" src/templates/`
---
## theme.css
```css
/* Colors from colorThemes.json */
:root {
--background: #050012;
--card: #040121;
--foreground: #f0e6ff;
--primary-cta: #c89bff;
--primary-cta-text: #050012;
--secondary-cta: #1d123b;
--secondary-cta-text: #f0e6ff;
--accent: #684f7b;
--background-accent: #65417c;
}
/* Buttons from theme-options/buttons/ */
.primary-button { /* ... */ }
.secondary-button { /* ... */ }
/* Cards from theme-options/cards/ */
.card { /* ... */ }
```
---
## Creating a Template
1. Create `src/templates/[name]/page.tsx` and `theme.css`
2. Define types matching your chosen sections
3. Create defaultProps with demo content
4. Copy section code inline (don't import sections)
5. Add base markers to each section
6. Build theme.css from colorThemes.json + theme-options/
---
## Updating Templates
When a base section changes, find affected templates with grep and apply the fix to each inline section.
---
## Not Included
- Not in registry.json (standalone files)
- No ThemeProvider
- No imported sections (inline code only)

View File

@@ -0,0 +1,58 @@
================================================================================
THEME OPTIONS (v4)
================================================================================
STYLE PROPS
-----------
1. borderRadius
• "rounded"
• "soft"
• "pill"
2. contentWidth
• "small"
• "compact"
• "medium"
• "mediumLarge"
3. cardStyle
• "solid"
• "outline"
• "gradient-mesh"
• "gradient-radial"
• "inset"
• "glass-elevated"
• "glass-depth"
• "gradient-bordered"
• "layered-gradient"
• "soft-shadow"
• "subtle-shadow"
• "elevated-border"
• "inner-glow"
• "spotlight"
4. primaryButtonStyle
• "gradient"
• "shadow"
• "flat"
• "radial-glow"
• "diagonal-gradient"
• "double-inset"
• "primary-glow"
• "inset-glow"
• "soft-glow"
• "glass-shimmer"
• "neon-outline"
• "lifted"
• "depth-layers"
• "accent-edge"
• "metallic"
5. secondaryButtonStyle
• "glass"
• "solid"
• "layered"
• "radial-glow"
================================================================================

273
UI-COMPONENTS-RESEARCH.md Normal file
View File

@@ -0,0 +1,273 @@
# UI Components Research
## Common Across All (48 components)
All three platforms (Lovable, Replit, Base44) share these - likely shadcn/ui base:
- accordion
- alert-dialog
- alert
- aspect-ratio
- avatar
- badge
- breadcrumb
- button
- calendar
- card
- carousel
- chart
- checkbox
- collapsible
- command
- context-menu
- dialog
- drawer
- dropdown-menu
- form
- hover-card
- input-otp
- input
- label
- menubar
- navigation-menu
- pagination
- popover
- progress
- radio-group
- resizable
- scroll-area
- select
- separator
- sheet
- sidebar
- skeleton
- slider
- sonner
- switch
- table
- tabs
- textarea
- toast
- toaster
- toggle-group
- toggle
- tooltip
## Replit-Only (7 unique)
- button-group
- empty
- field
- input-group
- item
- kbd
- spinner
---
## Lovable
- accordion
- alert-dialog
- alert
- aspect-ratio
- avatar
- badge
- breadcrumb
- button
- calendar
- card
- carousel
- chart
- checkbox
- collapsible
- command
- context-menu
- dialog
- drawer
- dropdown-menu
- form
- hover-card
- input-otp
- input
- label
- menubar
- navigation-menu
- pagination
- popover
- progress
- radio-group
- resizable
- scroll-area
- select
- separator
- sheet
- sidebar
- skeleton
- slider
- sonner (toast notifications)
- switch
- table
- tabs
- textarea
- toast
- toaster
- toggle-group
- toggle
- tooltip
## Replit
- accordion
- alert-dialog
- alert
- aspect-ratio
- avatar
- badge
- breadcrumb
- button-group
- button
- calendar
- card
- carousel
- chart
- checkbox
- collapsible
- command
- context-menu
- dialog
- drawer
- dropdown-menu
- empty
- field
- form
- hover-card
- input-group
- input-otp
- input
- item
- kbd (keyboard shortcut display)
- label
- menubar
- navigation-menu
- pagination
- popover
- progress
- radio-group
- resizable
- scroll-area
- select
- separator
- sheet
- sidebar
- skeleton
- slider
- sonner
- spinner
- switch
- table
- tabs
- textarea
- toast
- toaster
- toggle-group
- toggle
- tooltip
## Base44
- accordion
- alert-dialog
- alert
- aspect-ratio
- avatar
- badge
- breadcrumb
- button
- calendar
- card
- carousel
- chart
- checkbox
- collapsible
- command
- context-menu
- dialog
- drawer
- dropdown-menu
- form
- hover-card
- input-otp
- input
- label
- menubar
- navigation-menu
- pagination
- popover
- progress
- radio-group
- resizable
- scroll-area
- select
- separator
- sheet
- sidebar
- skeleton
- slider
- sonner
- switch
- table
- tabs
- textarea
- toast
- toaster
- toggle-group
- toggle
- tooltip
---
## Analysis Summary
### Key Patterns from Competitors
1. **forwardRef** - All components use `React.forwardRef` for ref access
2. **CVA** - Class Variance Authority for variant management
3. **Compound exports** - Multiple sub-components per file
4. **Radix primitives** - 15+ Radix packages for accessibility
5. **CSS animations** - `data-[state=open]:animate-in` patterns
### Our Differences
| Aspect | Competitors | Our Approach | Why |
|--------|-------------|--------------|-----|
| Animation | CSS only | Framer Motion | More powerful for marketing sites |
| Variants | CVA | CSS classes | AI edits CSS directly |
| Exports | Compound | Single default | Simpler for AI |
| Backgrounds | None | 13 components | Unique value for marketing |
---
## Action Items
### 1. New Components to Add
| Component | Purpose | Approach |
|-----------|---------|----------|
### 2. Dependencies to Install
```bash
pnpm add embla-carousel-react react-day-picker
```
**No Radix dependencies** - Dropdown and Tooltip built from scratch with Framer Motion.
---
## Research Files
Competitor component files stored in:
- `research/base44/` - 49 JSX files
- `research/lovable/` - 48 TSX files
- `research/replit/` - 53 TSX files

598
colorThemes.css Normal file
View File

@@ -0,0 +1,598 @@
/* ============================================
COLOR THEMES - Generated from colorThemes.json
============================================ */
/* ============================================
LIGHT THEMES
============================================ */
/* @colorThemes/lightTheme/darkBlue */
/*
--background: #f5faff;
--card: #ffffff;
--foreground: #001122;
--primary-cta: #15479c;
--primary-cta-text: #f5faff;
--secondary-cta: #ffffff;
--secondary-cta-text: #001122;
--accent: #a8cce8;
--background-accent: #7ba3cf;
*/
/* @colorThemes/lightTheme/darkGreen */
/*
--background: #fafffb;
--card: #ffffff;
--foreground: #001a0a;
--primary-cta: #0a705f;
--primary-cta-text: #fafffb;
--secondary-cta: #ffffff;
--secondary-cta-text: #001a0a;
--accent: #a8d9be;
--background-accent: #6bbfb8;
*/
/* @colorThemes/lightTheme/lightRed */
/*
--background: #fffafa;
--card: #ffffff;
--foreground: #1a0000;
--primary-cta: #e63946;
--primary-cta-text: #fffafa;
--secondary-cta: #ffffff;
--secondary-cta-text: #1a0000;
--accent: #f5c4c7;
--background-accent: #f09199;
*/
/* @colorThemes/lightTheme/lightPurple */
/*
--background: #fbfaff;
--card: #ffffff;
--foreground: #0f0022;
--primary-cta: #8b5cf6;
--primary-cta-text: #fbfaff;
--secondary-cta: #ffffff;
--secondary-cta-text: #0f0022;
--accent: #d8cef5;
--background-accent: #c4a8f9;
*/
/* @colorThemes/lightTheme/warmCream */
/*
--background: #f6f0e9;
--card: #efe7dd;
--foreground: #2b180a;
--primary-cta: #2b180a;
--primary-cta-text: #f6f0e9;
--secondary-cta: #efe7dd;
--secondary-cta-text: #2b180a;
--accent: #94877c;
--background-accent: #afa094;
*/
/* @colorThemes/lightTheme/grayBlueAccent */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1c1c1c;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #15479c;
--background-accent: #a8cce8;
*/
/* @colorThemes/lightTheme/grayGreenAccent */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1c1c1c;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #159c49;
--background-accent: #a8e8ba;
*/
/* @colorThemes/lightTheme/grayRedAccent */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1c1c1c;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #e63946;
--background-accent: #e8bea8;
*/
/* @colorThemes/lightTheme/grayPurpleAccent */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1c1c1c;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #6139e6;
--background-accent: #b3a8e8;
*/
/* @colorThemes/lightTheme/warmBeige */
/*
--background: #efebe5;
--card: #f7f2ea;
--foreground: #000000;
--primary-cta: #000000;
--primary-cta-text: #efebe5;
--secondary-cta: #ffffff;
--secondary-cta-text: #000000;
--accent: #ffffff;
--background-accent: #e1b875;
*/
/* @colorThemes/lightTheme/grayTealGreen */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1f514c;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #159c49;
--background-accent: #a8e8ba;
*/
/* @colorThemes/lightTheme/grayNavyBlue */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #1f3251;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #15479c;
--background-accent: #a8cce8;
*/
/* @colorThemes/lightTheme/grayBurgundyRed */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #511f1f;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #e63946;
--background-accent: #e8bea8;
*/
/* @colorThemes/lightTheme/grayIndigoPurple */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #341f51;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #6139e6;
--background-accent: #b3a8e8;
*/
/* @colorThemes/lightTheme/warmgrayPink */
/*
--background: #f7f6f7;
--card: #ffffff;
--foreground: #1b0c25;
--primary-cta: #1b0c25;
--primary-cta-text: #f7f6f7;
--secondary-cta: #ffffff;
--secondary-cta-text: #1b0c25;
--accent: #ff93e4;
--background-accent: #e8a8c3;
*/
/* @colorThemes/lightTheme/warmgrayOrange */
/*
--background: #f7f6f7;
--card: #ffffff;
--foreground: #25190c;
--primary-cta: #ff6207;
--primary-cta-text: #f7f6f7;
--secondary-cta: #ffffff;
--secondary-cta-text: #25190c;
--accent: #ffce93;
--background-accent: #e8cfa8;
*/
/* @colorThemes/lightTheme/warmgrayBlue */
/*
--background: #f7f6f7;
--card: #ffffff;
--foreground: #0c1325;
--primary-cta: #0798ff;
--primary-cta-text: #f7f6f7;
--secondary-cta: #ffffff;
--secondary-cta-text: #0c1325;
--accent: #93c7ff;
--background-accent: #a8cde8;
*/
/* @colorThemes/lightTheme/lavenderPeach */
/*
--background: #e3deea;
--card: #ffffff;
--foreground: #27231f;
--primary-cta: #27231f;
--primary-cta-text: #e3deea;
--secondary-cta: #ffffff;
--secondary-cta-text: #27231f;
--accent: #c68a62;
--background-accent: #c68a62;
*/
/* @colorThemes/lightTheme/lavenderBlue */
/*
--background: #e3deea;
--card: #ffffff;
--foreground: #1f2027;
--primary-cta: #1f2027;
--primary-cta-text: #e3deea;
--secondary-cta: #ffffff;
--secondary-cta-text: #1f2027;
--accent: #627dc6;
--background-accent: #627dc6;
*/
/* @colorThemes/lightTheme/warmStone */
/*
--background: #f5f4ef;
--card: #dad6cd;
--foreground: #2a2928;
--primary-cta: #2a2928;
--primary-cta-text: #f5f4ef;
--secondary-cta: #ecebea;
--secondary-cta-text: #2a2928;
--accent: #ffffff;
--background-accent: #c6b180;
*/
/* @colorThemes/lightTheme/warmStoneGray */
/*
--background: #f5f4f0;
--card: #ffffff;
--foreground: #1a1a1a;
--primary-cta: #2c2c2c;
--primary-cta-text: #f5f4f0;
--secondary-cta: #f5f4f0;
--secondary-cta-text: #1a1a1a;
--accent: #8a8a8a;
--background-accent: #e8e6e1;
*/
/* @colorThemes/lightTheme/warmGreen */
/*
--background: #f6f7f4;
--card: #fffefe;
--foreground: #080908;
--primary-cta: #0e3a29;
--primary-cta-text: #fffefe;
--secondary-cta: #ebeee0;
--secondary-cta-text: #080908;
--accent: #35c18b;
--background-accent: #c6efc6;
*/
/* @colorThemes/lightTheme/warmSand */
/*
--background: #fcf6ec;
--card: #f3ede2;
--foreground: #2e2521;
--primary-cta: #2e2521;
--primary-cta-text: #fcf6ec;
--secondary-cta: #ffffff;
--secondary-cta-text: #2e2521;
--accent: #b2a28b;
--background-accent: #b2a28b;
*/
/* @colorThemes/lightTheme/warmgrayRed */
/*
--background: #f7f6f7;
--card: #ffffff;
--foreground: #250c0d;
--primary-cta: #b82b40;
--primary-cta-text: #f7f6f7;
--secondary-cta: #ffffff;
--secondary-cta-text: #250c0d;
--accent: #b90941;
--background-accent: #e8a8b6;
*/
/* @colorThemes/lightTheme/grayBurgundyCoral */
/*
--background: #f5f5f5;
--card: #ffffff;
--foreground: #1c1c1c;
--primary-cta: #511f1f;
--primary-cta-text: #f5f5f5;
--secondary-cta: #ffffff;
--secondary-cta-text: #1c1c1c;
--accent: #8f3838;
--background-accent: #c9725c;
*/
/* ============================================
DARK THEMES
============================================ */
/* @colorThemes/darkTheme/minimal */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #ffffffe6;
--primary-cta: #e6e6e6;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #ffffffe6;
--accent: #737373;
--background-accent: #737373;
*/
/* @colorThemes/darkTheme/minimalLightBlue */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f0f8ffe6;
--primary-cta: #cee7ff;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #f0f8ffe6;
--accent: #737373;
--background-accent: #737373;
*/
/* @colorThemes/darkTheme/minimalLightGreen */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f5fffae6;
--primary-cta: #80da9b;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #f5fffae6;
--accent: #737373;
--background-accent: #737373;
*/
/* @colorThemes/darkTheme/minimalLightRed */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #fff5f5e6;
--primary-cta: #ff7a7a;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #fff5f5e6;
--accent: #737373;
--background-accent: #737373;
*/
/* @colorThemes/darkTheme/minimalLightPurple */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f8f5ffe6;
--primary-cta: #c89bff;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #f8f5ffe6;
--accent: #737373;
--background-accent: #737373;
*/
/* @colorThemes/darkTheme/lime */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f5f5f5;
--primary-cta: #dfff1c;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #ffffff;
--accent: #8b9a1b;
--background-accent: #5d6b00;
*/
/* @colorThemes/darkTheme/gold */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f5f5f5;
--primary-cta: #ffdf7d;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta-text: #ffffff;
--accent: #b8860b;
--background-accent: #8b6914;
*/
/* @colorThemes/darkTheme/midnightBlue */
/*
--background: #000000;
--card: #0c0c0c;
--foreground: #ffffff;
--primary-cta: #106EFB;
--primary-cta-text: #ffffff;
--secondary-cta: #000000;
--secondary-cta-text: #ffffff;
--accent: #535353;
--background-accent: #106EFB;
*/
/* @colorThemes/darkTheme/blueOrangeAccent */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #ffffff;
--primary-cta: #1f7cff;
--primary-cta-text: #ffffff;
--secondary-cta: #010101;
--secondary-cta-text: #ffffff;
--accent: #1f7cff;
--background-accent: #f96b2f;
*/
/* @colorThemes/darkTheme/minimalBrightOrange */
/*
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #ffffff;
--primary-cta: #e34400;
--primary-cta-text: #ffffff;
--secondary-cta: #010101;
--secondary-cta-text: #ffffff;
--accent: #737373;
--background-accent: #e34400;
*/
/* @colorThemes/darkTheme/lightBlue */
/*
--background: #010912;
--card: #152840;
--foreground: #e6f0ff;
--primary-cta: #cee7ff;
--primary-cta-text: #010912;
--secondary-cta: #0e1a29;
--secondary-cta-text: #e6f0ff;
--accent: #3f5c79;
--background-accent: #004a93;
*/
/* @colorThemes/darkTheme/lightGreen */
/*
--background: #000802;
--card: #0b1a0b;
--foreground: #e6ffe6;
--primary-cta: #80da9b;
--primary-cta-text: #000802;
--secondary-cta: #07170b;
--secondary-cta-text: #e6ffe6;
--accent: #38714a;
--background-accent: #2c6541;
*/
/* @colorThemes/darkTheme/lightRed */
/*
--background: #080000;
--card: #1e0d0d;
--foreground: #ffe6e6;
--primary-cta: #ff7a7a;
--primary-cta-text: #080000;
--secondary-cta: #1e0909;
--secondary-cta-text: #ffe6e6;
--accent: #7b4242;
--background-accent: #65292c;
*/
/* @colorThemes/darkTheme/darkRed */
/*
--background: #060000;
--card: #1d0d0d;
--foreground: #ffe6e6;
--primary-cta: #ff3d4a;
--primary-cta-text: #ffffff;
--secondary-cta: #1f0a0a;
--secondary-cta-text: #ffe6e6;
--accent: #7b2d2d;
--background-accent: #b8111f;
*/
/* @colorThemes/darkTheme/lightPurple */
/*
--background: #050012;
--card: #040121;
--foreground: #f0e6ff;
--primary-cta: #c89bff;
--primary-cta-text: #050012;
--secondary-cta: #1d123b;
--secondary-cta-text: #f0e6ff;
--accent: #684f7b;
--background-accent: #65417c;
*/
/* @colorThemes/darkTheme/lightOrange */
/*
--background: #080200;
--card: #1a0d0b;
--foreground: #ffe6d5;
--primary-cta: #ffaa70;
--primary-cta-text: #080200;
--secondary-cta: #170b07;
--secondary-cta-text: #ffe6d5;
--accent: #7b5e4a;
--background-accent: #b8541e;
*/
/* @colorThemes/darkTheme/deepBlue */
/*
--background: #020617;
--card: #0f172a;
--foreground: #e2e8f0;
--primary-cta: #c4d8f9;
--primary-cta-text: #020617;
--secondary-cta: #041633;
--secondary-cta-text: #e2e8f0;
--accent: #2d30f3;
--background-accent: #1d4ed8;
*/
/* @colorThemes/darkTheme/violet */
/*
--background: #030128;
--card: #241f48;
--foreground: #ffffff;
--primary-cta: #ffffff;
--primary-cta-text: #030128;
--secondary-cta: #131136;
--secondary-cta-text: #d5d4f6;
--accent: #44358a;
--background-accent: #b597fe;
*/
/* @colorThemes/darkTheme/ruby */
/*
--background: #000000;
--card: #481f1f;
--foreground: #ffffff;
--primary-cta: #ffffff;
--primary-cta-text: #280101;
--secondary-cta: #361311;
--secondary-cta-text: #f6d4d4;
--accent: #51000b;
--background-accent: #ff2231;
*/
/* @colorThemes/darkTheme/emerald */
/*
--background: #000000;
--card: #1f4035;
--foreground: #ffffff;
--primary-cta: #ffffff;
--primary-cta-text: #051a12;
--secondary-cta: #0d2b1f;
--secondary-cta-text: #d4f6e8;
--accent: #0d5238;
--background-accent: #10b981;
*/

501
colorThemes.json Normal file
View File

@@ -0,0 +1,501 @@
{
"lightTheme": {
"darkBlue": {
"--background": "#f5faff",
"--card": "#ffffff",
"--foreground": "#001122",
"--primary-cta": "#15479c",
"--secondary-cta": "#ffffff",
"--accent": "#a8cce8",
"--background-accent": "#7ba3cf",
"--primary-cta-text": "#f5faff",
"--secondary-cta-text": "#001122"
},
"darkGreen": {
"--background": "#fafffb",
"--card": "#ffffff",
"--foreground": "#001a0a",
"--primary-cta": "#0a705f",
"--secondary-cta": "#ffffff",
"--accent": "#a8d9be",
"--background-accent": "#6bbfb8",
"--primary-cta-text": "#fafffb",
"--secondary-cta-text": "#001a0a"
},
"lightRed": {
"--background": "#fffafa",
"--card": "#ffffff",
"--foreground": "#1a0000",
"--primary-cta": "#e63946",
"--secondary-cta": "#ffffff",
"--accent": "#f5c4c7",
"--background-accent": "#f09199",
"--primary-cta-text": "#fffafa",
"--secondary-cta-text": "#1a0000"
},
"lightPurple": {
"--background": "#fbfaff",
"--card": "#ffffff",
"--foreground": "#0f0022",
"--primary-cta": "#8b5cf6",
"--secondary-cta": "#ffffff",
"--accent": "#d8cef5",
"--background-accent": "#c4a8f9",
"--primary-cta-text": "#fbfaff",
"--secondary-cta-text": "#0f0022"
},
"warmCream": {
"--background": "#f6f0e9",
"--card": "#efe7dd",
"--foreground": "#2b180a",
"--primary-cta": "#2b180a",
"--secondary-cta": "#efe7dd",
"--accent": "#94877c",
"--background-accent": "#afa094",
"--primary-cta-text": "#f6f0e9",
"--secondary-cta-text": "#2b180a"
},
"grayBlueAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#15479c",
"--background-accent": "#a8cce8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayGreenAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#159c49",
"--background-accent": "#a8e8ba",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayRedAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#e63946",
"--background-accent": "#e8bea8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayPurpleAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#6139e6",
"--background-accent": "#b3a8e8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"warmBeige": {
"--background": "#efebe5",
"--card": "#f7f2ea",
"--foreground": "#000000",
"--primary-cta": "#000000",
"--secondary-cta": "#ffffff",
"--accent": "#ffffff",
"--background-accent": "#e1b875",
"--primary-cta-text": "#efebe5",
"--secondary-cta-text": "#000000"
},
"grayTealGreen": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1f514c",
"--secondary-cta": "#ffffff",
"--accent": "#159c49",
"--background-accent": "#a8e8ba",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayNavyBlue": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1f3251",
"--secondary-cta": "#ffffff",
"--accent": "#15479c",
"--background-accent": "#a8cce8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayBurgundyRed": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#511f1f",
"--secondary-cta": "#ffffff",
"--accent": "#e63946",
"--background-accent": "#e8bea8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayIndigoPurple": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#341f51",
"--secondary-cta": "#ffffff",
"--accent": "#6139e6",
"--background-accent": "#b3a8e8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"warmgrayPink": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#1b0c25",
"--primary-cta": "#1b0c25",
"--secondary-cta": "#ffffff",
"--accent": "#ff93e4",
"--background-accent": "#e8a8c3",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#1b0c25"
},
"warmgrayOrange": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#25190c",
"--primary-cta": "#ff6207",
"--secondary-cta": "#ffffff",
"--accent": "#ffce93",
"--background-accent": "#e8cfa8",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#25190c"
},
"warmgrayBlue": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#0c1325",
"--primary-cta": "#0798ff",
"--secondary-cta": "#ffffff",
"--accent": "#93c7ff",
"--background-accent": "#a8cde8",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#0c1325"
},
"lavenderPeach": {
"--background": "#e3deea",
"--card": "#ffffff",
"--foreground": "#27231f",
"--primary-cta": "#27231f",
"--secondary-cta": "#ffffff",
"--accent": "#c68a62",
"--background-accent": "#c68a62",
"--primary-cta-text": "#e3deea",
"--secondary-cta-text": "#27231f"
},
"lavenderBlue": {
"--background": "#e3deea",
"--card": "#ffffff",
"--foreground": "#1f2027",
"--primary-cta": "#1f2027",
"--secondary-cta": "#ffffff",
"--accent": "#627dc6",
"--background-accent": "#627dc6",
"--primary-cta-text": "#e3deea",
"--secondary-cta-text": "#1f2027"
},
"warmStone": {
"--background": "#f5f4ef",
"--card": "#dad6cd",
"--foreground": "#2a2928",
"--primary-cta": "#2a2928",
"--secondary-cta": "#ecebea",
"--accent": "#ffffff",
"--background-accent": "#c6b180",
"--primary-cta-text": "#f5f4ef",
"--secondary-cta-text": "#2a2928"
},
"warmStoneGray": {
"--background": "#f5f4f0",
"--card": "#ffffff",
"--foreground": "#1a1a1a",
"--primary-cta": "#2c2c2c",
"--secondary-cta": "#f5f4f0",
"--accent": "#8a8a8a",
"--background-accent": "#e8e6e1",
"--primary-cta-text": "#f5f4f0",
"--secondary-cta-text": "#1a1a1a"
},
"warmGreen": {
"--background": "#f6f7f4",
"--card": "#fffefe",
"--foreground": "#080908",
"--primary-cta": "#0e3a29",
"--secondary-cta": "#ebeee0",
"--accent": "#35c18b",
"--background-accent": "#c6efc6",
"--primary-cta-text": "#fffefe",
"--secondary-cta-text": "#080908"
},
"warmSand": {
"--background": "#fcf6ec",
"--card": "#f3ede2",
"--foreground": "#2e2521",
"--primary-cta": "#2e2521",
"--secondary-cta": "#ffffff",
"--accent": "#b2a28b",
"--background-accent": "#b2a28b",
"--primary-cta-text": "#fcf6ec",
"--secondary-cta-text": "#2e2521"
},
"warmgrayRed": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#250c0d",
"--primary-cta": "#b82b40",
"--secondary-cta": "#ffffff",
"--accent": "#b90941",
"--background-accent": "#e8a8b6",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#250c0d"
},
"grayBurgundyCoral": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#511f1f",
"--secondary-cta": "#ffffff",
"--accent": "#8f3838",
"--background-accent": "#c9725c",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
}
},
"darkTheme": {
"minimal": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffffe6",
"--primary-cta": "#e6e6e6",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffffe6"
},
"minimalLightBlue": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f0f8ffe6",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f0f8ffe6"
},
"minimalLightGreen": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5fffae6",
"--primary-cta": "#80da9b",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f5fffae6"
},
"minimalLightRed": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#fff5f5e6",
"--primary-cta": "#ff7a7a",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#fff5f5e6"
},
"minimalLightPurple": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f8f5ffe6",
"--primary-cta": "#c89bff",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f8f5ffe6"
},
"lime": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5f5f5",
"--primary-cta": "#dfff1c",
"--secondary-cta": "#1a1a1a",
"--accent": "#8b9a1b",
"--background-accent": "#5d6b00",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffff"
},
"gold": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5f5f5",
"--primary-cta": "#ffdf7d",
"--secondary-cta": "#1a1a1a",
"--accent": "#b8860b",
"--background-accent": "#8b6914",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffff"
},
"midnightBlue": {
"--background": "#000000",
"--card": "#0c0c0c",
"--foreground": "#ffffff",
"--primary-cta": "#106EFB",
"--secondary-cta": "#000000",
"--accent": "#535353",
"--background-accent": "#106EFB",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"blueOrangeAccent": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffff",
"--primary-cta": "#1f7cff",
"--secondary-cta": "#010101",
"--accent": "#1f7cff",
"--background-accent": "#f96b2f",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"minimalBrightOrange": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffff",
"--primary-cta": "#e34400",
"--secondary-cta": "#010101",
"--accent": "#737373",
"--background-accent": "#e34400",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"lightBlue": {
"--background": "#010912",
"--card": "#152840",
"--foreground": "#e6f0ff",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#0e1a29",
"--accent": "#3f5c79",
"--background-accent": "#004a93",
"--primary-cta-text": "#010912",
"--secondary-cta-text": "#e6f0ff"
},
"lightGreen": {
"--background": "#000802",
"--card": "#0b1a0b",
"--foreground": "#e6ffe6",
"--primary-cta": "#80da9b",
"--secondary-cta": "#07170b",
"--accent": "#38714a",
"--background-accent": "#2c6541",
"--primary-cta-text": "#000802",
"--secondary-cta-text": "#e6ffe6"
},
"lightRed": {
"--background": "#080000",
"--card": "#1e0d0d",
"--foreground": "#ffe6e6",
"--primary-cta": "#ff7a7a",
"--secondary-cta": "#1e0909",
"--accent": "#7b4242",
"--background-accent": "#65292c",
"--primary-cta-text": "#080000",
"--secondary-cta-text": "#ffe6e6"
},
"darkRed": {
"--background": "#060000",
"--card": "#1d0d0d",
"--foreground": "#ffe6e6",
"--primary-cta": "#ff3d4a",
"--secondary-cta": "#1f0a0a",
"--accent": "#7b2d2d",
"--background-accent": "#b8111f",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffe6e6"
},
"lightPurple": {
"--background": "#050012",
"--card": "#040121",
"--foreground": "#f0e6ff",
"--primary-cta": "#c89bff",
"--secondary-cta": "#1d123b",
"--accent": "#684f7b",
"--background-accent": "#65417c",
"--primary-cta-text": "#050012",
"--secondary-cta-text": "#f0e6ff"
},
"lightOrange": {
"--background": "#080200",
"--card": "#1a0d0b",
"--foreground": "#ffe6d5",
"--primary-cta": "#ffaa70",
"--secondary-cta": "#170b07",
"--accent": "#7b5e4a",
"--background-accent": "#b8541e",
"--primary-cta-text": "#080200",
"--secondary-cta-text": "#ffe6d5"
},
"deepBlue": {
"--background": "#020617",
"--card": "#0f172a",
"--foreground": "#e2e8f0",
"--primary-cta": "#c4d8f9",
"--secondary-cta": "#041633",
"--accent": "#2d30f3",
"--background-accent": "#1d4ed8",
"--primary-cta-text": "#020617",
"--secondary-cta-text": "#e2e8f0"
},
"violet": {
"--background": "#030128",
"--card": "#241f48",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#131136",
"--accent": "#44358a",
"--background-accent": "#b597fe",
"--primary-cta-text": "#030128",
"--secondary-cta-text": "#d5d4f6"
},
"ruby": {
"--background": "#000000",
"--card": "#481f1f",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#361311",
"--accent": "#51000b",
"--background-accent": "#ff2231",
"--primary-cta-text": "#280101",
"--secondary-cta-text": "#f6d4d4"
},
"emerald": {
"--background": "#000000",
"--card": "#1f4035",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#0d2b1f",
"--accent": "#0d5238",
"--background-accent": "#10b981",
"--primary-cta-text": "#051a12",
"--secondary-cta-text": "#d4f6e8"
}
}
}

41
cssOptions.json Normal file
View File

@@ -0,0 +1,41 @@
{
"cards": {
"solid": "background: var(--color-card);",
"outline": "background: var(--color-card);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 25%, transparent);",
"gradient-mesh": "background:\n radial-gradient(at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0px, transparent 50%),\n radial-gradient(at 100% 0%, color-mix(in srgb, var(--color-accent) 10%, transparent) 0px, transparent 50%),\n radial-gradient(at 100% 100%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0px, transparent 50%),\n radial-gradient(at 0% 100%, color-mix(in srgb, var(--color-accent) 12%, transparent) 0px, transparent 50%),\n var(--color-card);",
"gradient-radial": "background: radial-gradient(circle at center, color-mix(in srgb, var(--color-card) 100%, var(--color-accent) 20%) 0%, var(--color-card) 90%);",
"inset": "background: color-mix(in srgb, var(--color-card) 95%, var(--color-accent) 5%);\nbox-shadow:\n inset 2px 2px 4px color-mix(in srgb, var(--color-foreground) 8%, transparent),\n inset -2px -2px 4px color-mix(in srgb, var(--color-background) 20%, transparent);",
"glass-elevated": "backdrop-filter: blur(8px);\nbackground: linear-gradient(to bottom right, color-mix(in srgb, var(--color-card) 80%, transparent), color-mix(in srgb, var(--color-card) 40%, transparent));\nbox-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\nborder: 1px solid var(--color-card);",
"glass-depth": "background: color-mix(in srgb, var(--color-card) 80%, transparent);\nbackdrop-filter: blur(14px);\nbox-shadow:\n inset 0 0 20px 0 color-mix(in srgb, var(--color-accent) 7.5%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 7.5%, transparent);",
"gradient-bordered": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-card) 100%, var(--color-accent) 5%) -35%, var(--color-card) 65%);\nbox-shadow: 0px 0px 10px 4px color-mix(in srgb, var(--color-accent) 4%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 15%, transparent);",
"layered-gradient": "background:\n linear-gradient(color-mix(in srgb, var(--color-accent) 6%, transparent) 0%, transparent 59.26%),\n linear-gradient(var(--color-card) 0%, var(--color-card) 100%),\n var(--color-card);\nbox-shadow:\n 20px 18px 7px color-mix(in srgb, var(--color-accent) 0%, transparent),\n 2px 2px 2px color-mix(in srgb, var(--color-accent) 6.5%, transparent),\n 1px 1px 2px color-mix(in srgb, var(--color-accent) 2%, transparent);\nborder: 2px solid var(--color-secondary-cta);",
"soft-shadow": "background: var(--color-card);\nbox-shadow: color-mix(in srgb, var(--color-accent) 10%, transparent) 0px 0.706592px 0.706592px -0.666667px, color-mix(in srgb, var(--color-accent) 8%, transparent) 0px 1.80656px 1.80656px -1.33333px, color-mix(in srgb, var(--color-accent) 7%, transparent) 0px 3.62176px 3.62176px -2px, color-mix(in srgb, var(--color-accent) 7%, transparent) 0px 6.8656px 6.8656px -2.66667px, color-mix(in srgb, var(--color-accent) 5%, transparent) 0px 13.6468px 13.6468px -3.33333px, color-mix(in srgb, var(--color-accent) 2%, transparent) 0px 30px 30px -4px, var(--color-background) 0px 3px 1px 0px inset;",
"subtle-shadow": "background: var(--color-card);\nbox-shadow: color-mix(in srgb, var(--color-foreground) 5%, transparent) 0px 4px 32px 0px;",
"elevated-border": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-card) 100%, var(--color-foreground) 3%) 0%, var(--color-card) 100%);\nbox-shadow: 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 8%, transparent), 0 4px 6px -1px color-mix(in srgb, var(--color-foreground) 5%, transparent), 0 10px 15px -3px color-mix(in srgb, var(--color-foreground) 4%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-foreground) 6%, transparent);",
"inner-glow": "background: var(--color-card);\nbox-shadow: inset 0 0 30px 0 color-mix(in srgb, var(--color-foreground) 4%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 8%, transparent), 0 4px 12px -4px color-mix(in srgb, var(--color-foreground) 8%, transparent);",
"spotlight": "background:\n radial-gradient(ellipse at 0% 0%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0%, transparent 50%),\n var(--color-card);\nbox-shadow: inset 1px 1px 0 0 color-mix(in srgb, var(--color-foreground) 10%, transparent), 0 4px 16px -4px color-mix(in srgb, var(--color-foreground) 10%, transparent);"
},
"primaryButtons": {
"gradient": "background: linear-gradient(to bottom, color-mix(in srgb, var(--color-primary-cta) 75%, transparent), var(--color-primary-cta));\nbox-shadow: color-mix(in srgb, var(--color-background) 25%, transparent) 0px 1px 1px 0px inset, color-mix(in srgb, var(--color-primary-cta) 15%, transparent) 3px 3px 3px 0px;",
"shadow": "background: var(--color-primary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-primary-cta) 40%, transparent);",
"flat": "background: var(--color-primary-cta);",
"radial-glow": "background:\n radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),\n radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),\n var(--color-primary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 30%, transparent);",
"diagonal-gradient": "background: linear-gradient(to bottom right, color-mix(in srgb, var(--color-primary-cta) 80%, transparent), var(--color-foreground));\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 30%, transparent);",
"double-inset": "background: var(--color-primary-cta);\nbox-shadow: color-mix(in srgb, var(--color-background) 15%, transparent) 0px 4px 10px 0px inset, color-mix(in srgb, var(--color-background) 15%, transparent) 0px -4px 8px 0px inset;",
"primary-glow": "background: var(--color-primary-cta);\nbox-shadow: color-mix(in srgb, var(--color-background) 20%, transparent) 0px 3px 1px 0px inset, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 0.839802px 0.503881px -0.3125px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 1.99048px 1.19429px -0.625px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 3.63084px 2.1785px -0.9375px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 6.03627px 3.62176px -1.25px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 9.74808px 5.84885px -1.5625px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 15.9566px 9.57398px -1.875px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 27.4762px 16.4857px -2.1875px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 50px 30px -2.5px;",
"inset-glow": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-primary-cta) 65%, var(--color-background)) -35%, var(--color-primary-cta) 65%);\nbox-shadow: 0 10px 18px -7px color-mix(in srgb, var(--color-background) 50%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 15%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"soft-glow": "background: radial-gradient(ellipse at 50% -20%, color-mix(in srgb, var(--color-primary-cta) 70%, var(--color-foreground)) 0%, var(--color-primary-cta) 70%);\nbox-shadow: 0 8px 24px -6px color-mix(in srgb, var(--color-primary-cta) 35%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"glass-shimmer": "background: linear-gradient(165deg, color-mix(in srgb, var(--color-primary-cta) 85%, var(--color-foreground)) 0%, var(--color-primary-cta) 40%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 100%);\nbox-shadow: inset 0 1px 1px 0 color-mix(in srgb, var(--color-foreground) 25%, transparent), inset 0 -1px 1px 0 color-mix(in srgb, var(--color-background) 15%, transparent), 0 4px 12px -2px color-mix(in srgb, var(--color-primary-cta) 25%, transparent);",
"neon-outline": "background: var(--color-primary-cta);\nbox-shadow: 0 0 5px color-mix(in srgb, var(--color-accent) 50%, transparent), 0 0 15px color-mix(in srgb, var(--color-accent) 30%, transparent), 0 0 30px color-mix(in srgb, var(--color-accent) 15%, transparent), inset 0 0 8px color-mix(in srgb, var(--color-accent) 10%, transparent);",
"lifted": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-primary-cta) 95%, var(--color-foreground)) 0%, var(--color-primary-cta) 50%, color-mix(in srgb, var(--color-primary-cta) 95%, var(--color-background)) 100%);\nbox-shadow: inset 0 2px 3px 0 color-mix(in srgb, var(--color-foreground) 20%, transparent), inset 0 -2px 3px 0 color-mix(in srgb, var(--color-background) 25%, transparent), 0 2px 4px -1px color-mix(in srgb, var(--color-background) 40%, transparent);",
"depth-layers": "background: var(--color-primary-cta);\nbox-shadow: 0 1px 2px color-mix(in srgb, var(--color-primary-cta) 20%, transparent), 0 2px 4px color-mix(in srgb, var(--color-primary-cta) 20%, transparent), 0 4px 8px color-mix(in srgb, var(--color-primary-cta) 15%, transparent), 0 8px 16px color-mix(in srgb, var(--color-primary-cta) 10%, transparent), 0 16px 32px color-mix(in srgb, var(--color-primary-cta) 5%, transparent);",
"accent-edge": "background: linear-gradient(180deg, var(--color-primary-cta) 0%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 100%);\nbox-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 60%, transparent), 0 4px 12px -2px color-mix(in srgb, var(--color-accent) 35%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"metallic": "background: linear-gradient(135deg, color-mix(in srgb, var(--color-primary-cta) 80%, var(--color-foreground)) 0%, var(--color-primary-cta) 25%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 50%, var(--color-primary-cta) 75%, color-mix(in srgb, var(--color-primary-cta) 85%, var(--color-foreground)) 100%);\nbox-shadow: inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 30%, transparent), 0 3px 8px -2px color-mix(in srgb, var(--color-background) 50%, transparent);"
},
"secondaryButtons": {
"glass": "backdrop-filter: blur(8px);\nbackground: linear-gradient(to bottom right, color-mix(in srgb, var(--color-secondary-cta) 80%, transparent), var(--color-secondary-cta));\nbox-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\nborder: 1px solid var(--color-secondary-cta);",
"solid": "background: var(--color-secondary-cta);",
"layered": "background:\n linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),\n linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),\n linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),\n linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),\n linear-gradient(color-mix(in srgb, var(--color-secondary-cta) 60%, transparent), color-mix(in srgb, var(--color-secondary-cta) 60%, transparent)),\n var(--color-secondary-cta);\nbox-shadow:\n 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);\nborder: 1px solid var(--color-secondary-cta);",
"radial-glow": "background:\n radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),\n radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),\n var(--color-secondary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);"
}
}

28
eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
rules: {
'react-hooks/set-state-in-effect': 'off',
'react-hooks/static-components': 'off',
'no-empty': ['error', { allowEmptyCatch: true }],
},
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

237
fontThemes.json Normal file
View File

@@ -0,0 +1,237 @@
{
"singleFonts": {
"interTight": {
"name": "Inter Tight",
"import": "import { Inter_Tight } from \"next/font/google\";",
"initialization": "const interTight = Inter_Tight({\n variable: \"--font-inter-tight\",\n subsets: [\"latin\"],\n weight: [\"100\", \"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\", \"900\"],\n});",
"className": "${interTight.variable}",
"cssVariable": "font-family: var(--font-inter-tight), sans-serif;"
},
"inter": {
"name": "Inter",
"import": "import { Inter } from \"next/font/google\";",
"initialization": "const inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"className": "${inter.variable}",
"cssVariable": "font-family: var(--font-inter), sans-serif;"
},
"poppins": {
"name": "Poppins",
"import": "import { Poppins } from \"next/font/google\";",
"initialization": "const poppins = Poppins({\n variable: \"--font-poppins\",\n subsets: [\"latin\"],\n weight: [\"100\", \"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\", \"900\"],\n});",
"className": "${poppins.variable}",
"cssVariable": "font-family: var(--font-poppins), sans-serif;"
},
"montserrat": {
"name": "Montserrat",
"import": "import { Montserrat } from \"next/font/google\";",
"initialization": "const montserrat = Montserrat({\n variable: \"--font-montserrat\",\n subsets: [\"latin\"],\n});",
"className": "${montserrat.variable}",
"cssVariable": "font-family: var(--font-montserrat), sans-serif;"
},
"roboto": {
"name": "Roboto",
"import": "import { Roboto } from \"next/font/google\";",
"initialization": "const roboto = Roboto({\n variable: \"--font-roboto\",\n subsets: [\"latin\"],\n weight: [\"100\", \"300\", \"400\", \"500\", \"700\", \"900\"],\n});",
"className": "${roboto.variable}",
"cssVariable": "font-family: var(--font-roboto), sans-serif;"
},
"openSans": {
"name": "Open Sans",
"import": "import { Open_Sans } from \"next/font/google\";",
"initialization": "const openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});",
"className": "${openSans.variable}",
"cssVariable": "font-family: var(--font-open-sans), sans-serif;"
},
"lato": {
"name": "Lato",
"import": "import { Lato } from \"next/font/google\";",
"initialization": "const lato = Lato({\n variable: \"--font-lato\",\n subsets: [\"latin\"],\n weight: [\"100\", \"300\", \"400\", \"700\", \"900\"],\n});",
"className": "${lato.variable}",
"cssVariable": "font-family: var(--font-lato), sans-serif;"
},
"dmSans": {
"name": "DM Sans",
"import": "import { DM_Sans } from \"next/font/google\";",
"initialization": "const dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});",
"className": "${dmSans.variable}",
"cssVariable": "font-family: var(--font-dm-sans), sans-serif;"
},
"manrope": {
"name": "Manrope",
"import": "import { Manrope } from \"next/font/google\";",
"initialization": "const manrope = Manrope({\n variable: \"--font-manrope\",\n subsets: [\"latin\"],\n});",
"className": "${manrope.variable}",
"cssVariable": "font-family: var(--font-manrope), sans-serif;"
},
"sourceSans3": {
"name": "Source Sans 3",
"import": "import { Source_Sans_3 } from \"next/font/google\";",
"initialization": "const sourceSans3 = Source_Sans_3({\n variable: \"--font-source-sans-3\",\n subsets: [\"latin\"],\n});",
"className": "${sourceSans3.variable}",
"cssVariable": "font-family: var(--font-source-sans-3), sans-serif;"
},
"publicSans": {
"name": "Public Sans",
"import": "import { Public_Sans } from \"next/font/google\";",
"initialization": "const publicSans = Public_Sans({\n variable: \"--font-public-sans\",\n subsets: [\"latin\"],\n});",
"className": "${publicSans.variable}",
"cssVariable": "font-family: var(--font-public-sans), sans-serif;"
},
"mulish": {
"name": "Mulish",
"import": "import { Mulish } from \"next/font/google\";",
"initialization": "const mulish = Mulish({\n variable: \"--font-mulish\",\n subsets: [\"latin\"],\n});",
"className": "${mulish.variable}",
"cssVariable": "font-family: var(--font-mulish), sans-serif;"
},
"nunito": {
"name": "Nunito",
"import": "import { Nunito } from \"next/font/google\";",
"initialization": "const nunito = Nunito({\n variable: \"--font-nunito\",\n subsets: [\"latin\"],\n});",
"className": "${nunito.variable}",
"cssVariable": "font-family: var(--font-nunito), sans-serif;"
},
"nunitoSans": {
"name": "Nunito Sans",
"import": "import { Nunito_Sans } from \"next/font/google\";",
"initialization": "const nunitoSans = Nunito_Sans({\n variable: \"--font-nunito-sans\",\n subsets: [\"latin\"],\n});",
"className": "${nunitoSans.variable}",
"cssVariable": "font-family: var(--font-nunito-sans), sans-serif;"
},
"raleway": {
"name": "Raleway",
"import": "import { Raleway } from \"next/font/google\";",
"initialization": "const raleway = Raleway({\n variable: \"--font-raleway\",\n subsets: [\"latin\"],\n});",
"className": "${raleway.variable}",
"cssVariable": "font-family: var(--font-raleway), sans-serif;"
},
"archivo": {
"name": "Archivo",
"import": "import { Archivo } from \"next/font/google\";",
"initialization": "const archivo = Archivo({\n variable: \"--font-archivo\",\n subsets: [\"latin\"],\n});",
"className": "${archivo.variable}",
"cssVariable": "font-family: var(--font-archivo), sans-serif;"
},
"figtree": {
"name": "Figtree",
"import": "import { Figtree } from \"next/font/google\";",
"initialization": "const figtree = Figtree({\n variable: \"--font-figtree\",\n subsets: [\"latin\"],\n});",
"className": "${figtree.variable}",
"cssVariable": "font-family: var(--font-figtree), sans-serif;"
}
},
"fontPairings": {
"interOpenSans": {
"name": "Inter + Open Sans",
"description": "Neutral headings with friendly body. Clean and approachable.",
"headingFont": "inter",
"bodyFont": "openSans",
"imports": "import { Inter } from \"next/font/google\";\nimport { Open_Sans } from \"next/font/google\";",
"initializations": "const inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});\n\nconst openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});",
"classNames": "${inter.variable} ${openSans.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-open-sans), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-inter), sans-serif;\n}",
"bodyFontFamily": "var(--font-open-sans), sans-serif",
"headingsFontFamily": "var(--font-inter), sans-serif"
}
},
"dmSansInter": {
"name": "DM Sans + Inter",
"description": "Modern geometric headings with neutral body. Contemporary and clean.",
"headingFont": "dmSans",
"bodyFont": "inter",
"imports": "import { DM_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${dmSans.variable} ${inter.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-dm-sans), sans-serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-dm-sans), sans-serif"
}
},
"manropeDmSans": {
"name": "Manrope + DM Sans",
"description": "Geometric headings with clean body. Modern and professional.",
"headingFont": "manrope",
"bodyFont": "dmSans",
"imports": "import { Manrope } from \"next/font/google\";\nimport { DM_Sans } from \"next/font/google\";",
"initializations": "const manrope = Manrope({\n variable: \"--font-manrope\",\n subsets: [\"latin\"],\n});\n\nconst dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});",
"classNames": "${manrope.variable} ${dmSans.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-dm-sans), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-manrope), sans-serif;\n}",
"bodyFontFamily": "var(--font-dm-sans), sans-serif",
"headingsFontFamily": "var(--font-manrope), sans-serif"
}
},
"publicSansInter": {
"name": "Public Sans + Inter",
"description": "Government-inspired headings with neutral body. Professional and trustworthy.",
"headingFont": "publicSans",
"bodyFont": "inter",
"imports": "import { Public_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const publicSans = Public_Sans({\n variable: \"--font-public-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${publicSans.variable} ${inter.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-public-sans), sans-serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-public-sans), sans-serif"
}
},
"mulishInter": {
"name": "Mulish + Inter",
"description": "Minimal headings with neutral body. Clean and modern.",
"headingFont": "mulish",
"bodyFont": "inter",
"imports": "import { Mulish } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const mulish = Mulish({\n variable: \"--font-mulish\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${mulish.variable} ${inter.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-mulish), sans-serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-mulish), sans-serif"
}
},
"montserratInter": {
"name": "Montserrat + Inter",
"description": "Geometric sans-serif headings with neutral body. Popular and reliable.",
"headingFont": "montserrat",
"bodyFont": "inter",
"imports": "import { Montserrat } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const montserrat = Montserrat({\n variable: \"--font-montserrat\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${montserrat.variable} ${inter.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-montserrat), sans-serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-montserrat), sans-serif"
}
},
"libreBaskervilleInter": {
"name": "Libre Baskerville + Inter",
"description": "Classic serif headings with neutral body. Elegant and readable.",
"headingFont": "libreBaskerville",
"bodyFont": "inter",
"imports": "import { Libre_Baskerville } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const libreBaskerville = Libre_Baskerville({\n variable: \"--font-libre-baskerville\",\n subsets: [\"latin\"],\n weight: [\"400\", \"700\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${libreBaskerville.variable} ${inter.variable}",
"globalsCss": {
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-libre-baskerville), serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-libre-baskerville), serif"
}
}
}
}

22
index.html Normal file
View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Store of Emotions | Find Calm in Every Moment</title>
<meta property="og:title" content="Store of Emotions | Find Calm in Every Moment" />
<meta name="twitter:title" content="Store of Emotions | Find Calm in Every Moment" />
<meta name="description" content="Curated products and rituals for emotional wellness and self-care. Discover candles, journals, and experiences designed to honor your feelings." />
<meta property="og:description" content="Curated products and rituals for emotional wellness and self-care. Discover candles, journals, and experiences designed to honor your feelings." />
<meta name="twitter:description" content="Curated products and rituals for emotional wellness and self-care. Discover candles, journals, and experiences designed to honor your feelings." />
<meta name="keywords" content="emotional wellness, self-care, mood-enhancing, stress relief, emotional balance, mindfulness, healing, self-love, journals, candles, wellness kits" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

42
package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "webild-components",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc --noEmit --project tsconfig.app.json"
},
"dependencies": {
"@rive-app/react-canvas": "^4.28.1",
"@tailwindcss/vite": "^4.2.2",
"clsx": "^2.1.1",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "0.575.0",
"motion": "^12.38.0",
"react": "^19.2.4",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.4"
}
}

2284
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

2018
registry.json Normal file

File diff suppressed because it is too large Load Diff

13
src/App.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import HomePage from './pages/HomePage';
export default function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
</Route>
</Routes>
);
}

120
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,120 @@
import FooterSimpleReveal from '@/components/sections/footer/FooterSimpleReveal';
import NavbarInline from '@/components/ui/NavbarInline';
import SiteBackgroundSlot from "@/components/ui/SiteBackgroundSlot";
import { Outlet } from 'react-router-dom';
import { StyleProvider } from "@/components/ui/StyleProvider";
export default function Layout() {
const navItems = [
{
"name": "Home",
"href": "#hero"
},
{
"name": "About",
"href": "#about"
},
{
"name": "Collections",
"href": "#products"
},
{
"name": "Experiences",
"href": "#pricing"
},
{
"name": "Testimonials",
"href": "#testimonials"
},
{
"name": "FAQ",
"href": "#faq"
},
{
"name": "Contact",
"href": "#contact"
}
];
return (
<StyleProvider buttonVariant="elastic" siteBackground="gridDots" heroBackground="gradientBars">
<SiteBackgroundSlot />
<NavbarInline
ctaButton={{
text: "Shop Now",
href: "#products",
}}
navItems={navItems} />
<main className="flex-grow">
<Outlet />
</main>
<FooterSimpleReveal
brand="Store of Emotions"
columns={[
{
title: "Shop",
items: [
{
label: "Collections",
href: "#products",
},
{
label: "Subscriptions",
href: "#pricing",
},
{
label: "New Arrivals",
href: "#",
},
],
},
{
title: "Support",
items: [
{
label: "FAQ",
href: "#faq",
},
{
label: "Contact Us",
href: "#contact",
},
{
label: "Shipping & Returns",
href: "#",
},
],
},
{
title: "Company",
items: [
{
label: "About Us",
href: "#about",
},
{
label: "Our Philosophy",
href: "#about",
},
{
label: "Blog",
href: "#",
},
],
},
]}
copyright="© 2024 Store of Emotions. All rights reserved."
links={[
{
label: "Privacy Policy",
href: "#",
},
{
label: "Terms of Service",
href: "#",
},
]}
/>
</StyleProvider>
);
}

View File

@@ -0,0 +1,126 @@
import { useEffect } from "react";
import { X, Plus, Minus, Trash2 } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import Button from "@/components/ui/Button";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type CartItem = {
id: string;
name: string;
price: string;
quantity: number;
imageSrc: string;
};
type ProductCartProps = {
isOpen: boolean;
onClose: () => void;
items: CartItem[];
total: string;
onQuantityChange?: (id: string, quantity: number) => void;
onRemove?: (id: string) => void;
onCheckout?: () => void;
};
const ProductCart = ({ isOpen, onClose, items, total, onQuantityChange, onRemove, onCheckout }: ProductCartProps) => {
useEffect(() => {
if (!isOpen) return;
const onKeyDown = (e: KeyboardEvent) => e.key === "Escape" && onClose();
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [isOpen, onClose]);
useEffect(() => {
document.body.style.overflow = isOpen ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-1001">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="absolute inset-0 bg-foreground/50"
onClick={onClose}
/>
<motion.aside
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="card absolute right-0 top-0 flex flex-col p-5 h-screen w-screen md:w-96"
>
<div className="flex items-center justify-between">
<h2 className="text-xl font-medium text-foreground">Cart ({items.length})</h2>
<button onClick={onClose} className="card flex items-center justify-center size-8 rounded cursor-pointer" aria-label="Close cart">
<X className="size-4 text-foreground" strokeWidth={1.5} />
</button>
</div>
<div className="mt-5 h-px w-full bg-foreground/10" />
<div className="flex-1 py-5 min-h-0 overflow-y-auto">
{items.length === 0 ? (
<p className="py-20 text-center text-sm text-foreground/50">Your cart is empty</p>
) : (
<div className="flex flex-col gap-5">
{items.map((item) => (
<div key={item.id} className="flex gap-4">
<div className="shrink-0 size-24 overflow-hidden rounded">
<ImageOrVideo imageSrc={item.imageSrc} className="size-full object-cover" />
</div>
<div className="flex flex-1 flex-col justify-between min-w-0">
<div className="flex items-start justify-between gap-2">
<h3 className="text-base font-medium text-foreground truncate">{item.name}</h3>
<p className="shrink-0 text-base font-medium text-foreground">{item.price}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => item.quantity > 1 && onQuantityChange?.(item.id, item.quantity - 1)}
className="card flex items-center justify-center size-8 rounded cursor-pointer"
>
<Minus className="size-4 text-foreground" strokeWidth={1.5} />
</button>
<span className="min-w-5 text-center text-sm font-medium text-foreground">{item.quantity}</span>
<button
onClick={() => onQuantityChange?.(item.id, item.quantity + 1)}
className="card flex items-center justify-center size-8 rounded cursor-pointer"
>
<Plus className="size-4 text-foreground" strokeWidth={1.5} />
</button>
<button
onClick={() => onRemove?.(item.id)}
className="card flex items-center justify-center ml-auto size-8 rounded cursor-pointer"
>
<Trash2 className="size-4 text-foreground" strokeWidth={1.5} />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex flex-col gap-5">
<div className="h-px w-full bg-foreground/10" />
<div className="flex items-center justify-between">
<span className="text-base font-medium text-foreground">Total</span>
<span className="text-base font-medium text-foreground">{total}</span>
</div>
<Button text="Checkout" onClick={onCheckout} variant="primary" className="w-full" />
</div>
</motion.aside>
</div>
)}
</AnimatePresence>
);
};
export default ProductCart;
export type { CartItem };

View File

@@ -0,0 +1,142 @@
import { Star, ArrowUpRight, Loader2 } from "lucide-react";
import { cls } from "@/lib/utils";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import useProducts from "@/hooks/useProducts";
import type { ProductVariant } from "./ProductDetailCard";
type CatalogProduct = {
id: string;
name: string;
price: string;
imageSrc: string;
category?: string;
rating?: number;
reviewCount?: string;
onClick?: () => void;
};
type ProductCatalogProps = {
products?: CatalogProduct[];
searchValue?: string;
onSearchChange?: (value: string) => void;
filters?: ProductVariant[];
};
const ProductCatalog = ({ products: productsProp, searchValue = "", onSearchChange, filters }: ProductCatalogProps) => {
const { products: fetchedProducts, isLoading } = useProducts();
const products: CatalogProduct[] = productsProp && productsProp.length > 0
? productsProp
: fetchedProducts.map((p) => ({
id: p.id,
name: p.name,
price: p.price,
imageSrc: p.imageSrc,
category: p.brand,
rating: p.rating,
reviewCount: p.reviewCount,
onClick: p.onProductClick,
}));
if (isLoading && (!productsProp || productsProp.length === 0)) {
return (
<section className="mx-auto py-20 w-content-width">
<div className="flex justify-center">
<Loader2 className="size-8 text-foreground animate-spin" strokeWidth={1.5} />
</div>
</section>
);
}
return (
<section className="mx-auto py-20 w-content-width">
{(onSearchChange || (filters && filters.length > 0)) && (
<div className="flex flex-col gap-5 mb-5 md:flex-row md:items-end">
{onSearchChange && (
<div className="flex flex-1 flex-col gap-2 min-w-32">
<label className="text-sm font-medium text-foreground">Search</label>
<input
type="text"
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="Search products..."
className="card px-4 h-9 w-full md:w-80 text-base text-foreground bg-transparent rounded focus:outline-none"
/>
</div>
)}
{filters && filters.length > 0 && (
<div className="flex gap-5 items-end">
{filters.map((filter) => (
<div key={filter.label} className="flex flex-col gap-2 min-w-32">
<label className="text-sm font-medium text-foreground">{filter.label}</label>
<div className="secondary-button flex items-center px-3 h-9 rounded">
<select
value={filter.selected}
onChange={(e) => filter.onChange(e.target.value)}
className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none"
>
{filter.options.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
</div>
))}
</div>
)}
</div>
)}
{products.length === 0 ? (
<p className="py-20 text-center text-sm text-foreground/50">No products found</p>
) : (
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{products.map((product) => (
<button
key={product.id}
onClick={product.onClick}
className="card group h-full flex flex-col gap-3 p-3 text-left rounded cursor-pointer"
>
<div className="relative aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={product.imageSrc} className="size-full object-cover transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 flex items-center justify-center transition-all duration-300 group-hover:bg-background/20 group-hover:backdrop-blur-xs">
<div className="primary-button flex items-center justify-center size-12 rounded-full opacity-0 scale-75 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100">
<ArrowUpRight className="size-5 text-primary-cta-text" strokeWidth={2} />
</div>
</div>
</div>
<div className="flex flex-col gap-2">
{product.category && (
<span className="secondary-button w-fit px-2 py-0.5 text-sm text-secondary-cta-text rounded">{product.category}</span>
)}
<div className="flex flex-col gap-1">
<h3 className="text-xl font-medium text-foreground truncate">{product.name}</h3>
{product.rating && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={cls("size-4 text-accent", i < Math.floor(product.rating || 0) ? "fill-accent" : "opacity-20")}
strokeWidth={1.5}
/>
))}
</div>
{product.reviewCount && (
<span className="text-sm text-foreground">({product.reviewCount})</span>
)}
</div>
)}
</div>
<p className="text-2xl font-medium text-foreground">{product.price}</p>
</div>
</button>
))}
</div>
)}
</section>
);
};
export default ProductCatalog;
export type { CatalogProduct };

View File

@@ -0,0 +1,137 @@
import { useState } from "react";
import { Star } from "lucide-react";
import { cls } from "@/lib/utils";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import Button from "@/components/ui/Button";
import Transition from "@/components/ui/Transition";
type ProductVariant = {
label: string;
options: string[];
selected: string;
onChange: (value: string) => void;
};
type ProductDetailCardProps = {
name: string;
price: string;
salePrice?: string;
images: string[];
description?: string;
rating?: number;
ribbon?: string;
inventoryStatus?: "in-stock" | "out-of-stock";
inventoryQuantity?: number;
sku?: string;
variants?: ProductVariant[];
quantity?: ProductVariant;
onAddToCart?: () => void;
onBuyNow?: () => void;
};
const ProductDetailCard = ({ name, price, salePrice, images, description, rating = 0, ribbon, inventoryStatus, inventoryQuantity, sku, variants, quantity, onAddToCart, onBuyNow }: ProductDetailCardProps) => {
const [selectedImage, setSelectedImage] = useState(0);
return (
<section className="mx-auto py-20 w-content-width">
<div className="flex flex-col gap-5 md:flex-row">
<div className="relative md:w-1/2">
<Transition key={selectedImage} className="card aspect-square overflow-hidden rounded" transitionType="fade" whileInView={false}>
<ImageOrVideo imageSrc={images[selectedImage]} className="size-full object-cover" />
</Transition>
{images.length > 1 && (
<div className="absolute right-3 top-0 bottom-0 flex flex-col gap-3 py-3 overflow-y-auto mask-fade-y">
{images.map((src, i) => (
<button
key={i}
onClick={() => setSelectedImage(i)}
className="group card relative shrink-0 size-16 overflow-hidden rounded cursor-pointer"
>
<ImageOrVideo imageSrc={src} className="size-full object-cover transition-transform duration-300 group-hover:scale-110" />
<div className={cls(
"absolute top-1 right-1 primary-button size-3 rounded-full transition-transform duration-300",
selectedImage === i ? "scale-100" : "scale-0 group-hover:scale-100"
)} />
</button>
))}
</div>
)}
</div>
<div className="card flex flex-col gap-5 p-5 md:w-1/2 rounded">
<div className="flex items-start justify-between gap-5">
<h2 className="flex-1 text-2xl font-medium text-foreground md:text-3xl">{name}</h2>
{ribbon && <span className="secondary-button shrink-0 px-3 py-1 text-sm font-medium rounded text-secondary-cta-text">{ribbon}</span>}
</div>
<div className="h-px w-full bg-foreground/10" />
<div className="flex items-center justify-between">
<p className="text-xl font-medium text-foreground md:text-2xl">
{salePrice ? (
<>
<span className="text-foreground/75 line-through mr-1">{price}</span>
<span>{salePrice}</span>
</>
) : (
price
)}
</p>
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className={cls("size-5 text-accent", i < Math.floor(rating) ? "fill-accent" : "opacity-20")} strokeWidth={1.5} />
))}
</div>
</div>
{(inventoryStatus || inventoryQuantity || sku) && (
<div className="flex flex-wrap gap-3 text-sm">
{inventoryStatus && (
<span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">
{inventoryStatus === "in-stock" ? "In Stock" : "Out of Stock"}
</span>
)}
{inventoryQuantity && (
<span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">{inventoryQuantity} available</span>
)}
{sku && <span className="secondary-button px-2 py-1 rounded text-secondary-cta-text">SKU: {sku}</span>}
</div>
)}
{description && <p className="text-sm text-foreground/75 md:text-base">{description}</p>}
{variants && variants.length > 0 && (
<div className="flex flex-wrap gap-5">
{variants.map((variant) => (
<div key={variant.label} className="flex flex-1 flex-col gap-2 min-w-32">
<label className="text-sm font-medium text-foreground">{variant.label}</label>
<div className="secondary-button flex items-center px-3 h-9 rounded">
<select value={variant.selected} onChange={(e) => variant.onChange(e.target.value)} className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none">
{variant.options.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
</div>
))}
</div>
)}
{quantity && (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-foreground">{quantity.label}</label>
<div className="secondary-button flex items-center px-3 h-9 w-24 rounded">
<select value={quantity.selected} onChange={(e) => quantity.onChange(e.target.value)} className="w-full text-base text-secondary-cta-text bg-transparent cursor-pointer focus:outline-none">
{quantity.options.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
</div>
)}
<div className="flex flex-col mt-auto gap-3 pt-5">
<Button text="Add To Cart" onClick={onAddToCart} variant="primary" className="w-full" />
<Button text="Buy Now" onClick={onBuyNow} variant="secondary" className="w-full" />
</div>
</div>
</div>
</section>
);
};
export default ProductDetailCard;
export type { ProductVariant };

View File

@@ -0,0 +1,13 @@
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
const DefaultsLayout = () => {
useEffect(() => {
document.body.classList.add('use-defaults');
return () => document.body.classList.remove('use-defaults');
}, []);
return <Outlet />;
};
export default DefaultsLayout;

View File

@@ -0,0 +1,142 @@
import { motion } from "motion/react";
import type { LucideIcon } from "lucide-react";
type AboutFeaturesSplitProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: { icon: string | LucideIcon; title: string; description: string }[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutFeaturesSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
imageSrc,
videoSrc,
}: AboutFeaturesSplitProps) => {
return (
<section
data-webild-section="AboutFeaturesSplit"
aria-label="About section"
className="relative w-full py-16 md:py-24"
>
<div className="flex flex-col gap-8 mx-auto w-content-width">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="flex flex-col md:flex-row md:items-stretch gap-5">
<div className="flex flex-col justify-center gap-5 p-5 w-full md:w-4/10 2xl:w-3/10 card rounded-theme">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-center shrink-0 mb-1 size-8 primary-button rounded-theme">
{typeof item.icon === "function" ? (
<item.icon className="h-2/5 w-2/5 text-primary-cta-text" strokeWidth={1.5} />
) : (
<span className="text-sm text-primary-cta-text">{index + 1}</span>
)}
</div>
<h3 className="text-xl font-medium">{item.title}</h3>
<p className="text-base leading-tight">{item.description}</p>
</div>
{index < items.length - 1 && (
<div className="mt-5 border-b border-accent/40" />
)}
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="p-5 w-full md:w-6/10 2xl:w-7/10 h-80 md:h-auto card rounded-theme overflow-hidden"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="About video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
</div>
</div>
</section>
);
};
export default AboutFeaturesSplit;

View File

@@ -0,0 +1,107 @@
import { motion } from "motion/react";
type AboutMediaOverlayProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutMediaOverlay = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: AboutMediaOverlayProps) => {
return (
<section
data-webild-section="AboutMediaOverlay"
aria-label="About section"
className="relative w-full py-16 md:py-24"
>
<div className="relative flex items-center justify-center py-8 md:py-8 mx-auto w-content-width rounded-theme overflow-hidden">
<div className="absolute inset-0">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="About video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
<div className="absolute inset-0 bg-foreground/40 backdrop-blur-xs pointer-events-none select-none" />
</div>
<div className="relative z-10 flex items-center justify-center px-5 py-8 mx-auto min-h-100 md:min-h-120 md:w-1/2 w-content-width">
<div className="flex flex-col items-center gap-2 text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="mb-1 px-3 py-1 text-sm card rounded-full"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance text-primary-cta-text"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-primary-cta-text"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
</div>
</div>
</section>
);
};
export default AboutMediaOverlay;

View File

@@ -0,0 +1,90 @@
import { motion } from "motion/react";
import { Quote } from "lucide-react";
type AboutTestimonialProps = {
tag: string;
quote: string;
author: string;
role: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutTestimonial = ({
tag,
quote,
author,
role,
imageSrc,
videoSrc,
}: AboutTestimonialProps) => {
return (
<section
data-webild-section="AboutTestimonial"
aria-label="Testimonial section"
className="relative w-full py-16 md:py-24"
>
<div className="grid grid-cols-1 md:grid-cols-5 gap-5 mx-auto w-content-width">
<div className="relative md:col-span-3 p-8 card rounded-theme">
<div className="absolute flex items-center justify-center -top-7 -left-7 md:-top-8 md:-left-8 size-12 primary-button rounded-theme">
<Quote className="size-5 text-primary-cta-text" strokeWidth={1.5} />
</div>
<div className="relative flex flex-col justify-center gap-5 py-8 h-full">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-fit px-3 py-1 mb-1 text-sm card rounded-full"
>
{tag}
</motion.span>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
className="text-3xl md:text-4xl font-medium leading-tight"
>
{quote}
</motion.p>
<div className="flex items-center gap-2">
<span className="text-base">{author}</span>
<span className="text-accent"></span>
<span className="text-base">{role}</span>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="md:col-span-2 aspect-square md:aspect-auto md:h-full card rounded-theme overflow-hidden"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Testimonial video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</motion.div>
</div>
</section>
);
};
export default AboutTestimonial;

View File

@@ -0,0 +1,56 @@
import { motion } from "motion/react";
interface AboutTextProps {
title: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
}
const AboutText = ({
title,
primaryButton,
secondaryButton,
}: AboutTextProps) => {
return (
<section
data-webild-section="AboutText"
aria-label="About section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-3 items-center">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="text-2xl md:text-5xl font-medium text-center leading-tight text-balance"
>
{title}
</motion.h2>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap gap-3 justify-center mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
</section>
);
};
export default AboutText;

View File

@@ -0,0 +1,79 @@
import { motion } from "motion/react";
interface AboutTextSplitProps {
title: string;
descriptions: string[];
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
}
const AboutTextSplit = ({
title,
descriptions,
primaryButton,
secondaryButton,
}: AboutTextSplitProps) => {
return (
<section
data-webild-section="AboutTextSplit"
aria-label="About section"
className="relative w-full py-16 md:py-24"
>
<div className="flex flex-col gap-8 mx-auto w-content-width">
<div className="flex flex-col md:flex-row gap-3 md:gap-8">
<div className="w-full md:w-1/2">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="text-7xl font-medium"
>
{title}
</motion.h2>
</div>
<div className="flex flex-col gap-5 w-full md:w-1/2">
{descriptions.map((desc, index) => (
<motion.p
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="text-base md:text-2xl leading-tight"
>
{desc}
</motion.p>
))}
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap max-md:justify-center gap-5">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
</div>
<div className="w-full border-b border-foreground/10" />
</div>
</section>
);
};
export default AboutTextSplit;

View File

@@ -0,0 +1,148 @@
import { motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
type BlogItem = {
category: string;
title: string;
excerpt: string;
authorName: string;
authorImageSrc: string;
date: string;
imageSrc: string;
href?: string;
onClick?: () => void;
};
type BlogMediaCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items?: BlogItem[];
};
const BlogMediaCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: BlogMediaCardsProps) => {
if (!items || items.length === 0) {
return null;
}
return (
<section
data-webild-section="BlogMediaCards"
aria-label="Blog section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{items.map((item, index) => (
<motion.a
key={index}
href={item.href ?? "#"}
onClick={item.onClick}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="card group flex flex-col justify-between gap-5 p-5 rounded-theme cursor-pointer"
>
<div className="flex flex-col gap-2">
<span className="card w-fit rounded-full px-2 py-0.5 text-xs mb-0.5">{item.category}</span>
<h3 className="text-2xl md:text-3xl font-medium leading-tight line-clamp-2">{item.title}</h3>
<p className="text-sm leading-tight opacity-75 line-clamp-2">{item.excerpt}</p>
<div className="flex items-center gap-3 mt-1">
<img
src={item.authorImageSrc}
alt={item.authorName}
className="size-8 rounded-full object-cover"
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{item.authorName}</span>
<span className="text-xs opacity-75">{item.date}</span>
</div>
</div>
</div>
<div className="relative aspect-square rounded-theme overflow-hidden">
<img
src={item.imageSrc}
alt={item.title}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
</span>
</div>
</div>
</motion.a>
))}
</div>
</div>
</section>
);
};
export default BlogMediaCards;

View File

@@ -0,0 +1,151 @@
import { motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
type BlogItem = {
category: string;
title: string;
excerpt: string;
authorName: string;
authorImageSrc: string;
date: string;
imageSrc: string;
href?: string;
onClick?: () => void;
};
type BlogSimpleCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items?: BlogItem[];
};
const BlogSimpleCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: BlogSimpleCardsProps) => {
if (!items || items.length === 0) {
return null;
}
return (
<section
data-webild-section="BlogSimpleCards"
aria-label="Blog section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{items.map((item, index) => (
<motion.a
key={index}
href={item.href ?? "#"}
onClick={item.onClick}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="card group flex flex-col gap-5 p-5 rounded-theme cursor-pointer"
>
<div className="relative aspect-4/3 rounded-theme overflow-hidden">
<img
src={item.imageSrc}
alt={item.title}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
</span>
</div>
</div>
<div className="flex flex-1 flex-col justify-between gap-5">
<div className="flex flex-col gap-2">
<span className="primary-button w-fit rounded-full px-2 py-0.5 text-xs text-primary-cta-text">
{item.category}
</span>
<h3 className="text-xl font-medium leading-tight mt-1">{item.title}</h3>
<p className="text-sm leading-tight opacity-75">{item.excerpt}</p>
</div>
<div className="flex items-center gap-3">
<img
src={item.authorImageSrc}
alt={item.authorName}
className="size-8 rounded-full object-cover"
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{item.authorName}</span>
<span className="text-xs opacity-75">{item.date}</span>
</div>
</div>
</div>
</motion.a>
))}
</div>
</div>
</section>
);
};
export default BlogSimpleCards;

View File

@@ -0,0 +1,158 @@
import { motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
type BlogItem = {
title: string;
excerpt: string;
authorName: string;
authorImageSrc: string;
date: string;
tags: string[];
imageSrc: string;
href?: string;
onClick?: () => void;
};
type BlogTagCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items?: BlogItem[];
};
const BlogTagCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: BlogTagCardsProps) => {
if (!items || items.length === 0) {
return null;
}
return (
<section
data-webild-section="BlogTagCards"
aria-label="Blog section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{items.map((item, index) => (
<motion.a
key={index}
href={item.href ?? "#"}
onClick={item.onClick}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="card group flex flex-col gap-5 p-5 rounded-theme cursor-pointer"
>
<div className="relative aspect-4/3 rounded-theme overflow-hidden">
<img
src={item.imageSrc}
alt={item.title}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:bg-background/20 group-hover:backdrop-blur-xs transition-all duration-300">
<span className="primary-button flex items-center justify-center size-8 rounded-full opacity-0 group-hover:opacity-100 scale-75 group-hover:scale-100 transition-all duration-300">
<ArrowUpRight className="size-4 text-primary-cta-text" strokeWidth={2} />
</span>
</div>
</div>
<div className="flex flex-1 flex-col justify-between gap-5">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<img
src={item.authorImageSrc}
alt={item.authorName}
className="size-5 rounded-full object-cover"
/>
<span className="text-xs opacity-75">
{item.authorName} {item.date}
</span>
</div>
<h3 className="text-xl font-medium leading-tight">{item.title}</h3>
<p className="text-sm leading-tight opacity-75">{item.excerpt}</p>
</div>
<div className="flex flex-wrap gap-2">
{item.tags.map((itemTag) => (
<span
key={itemTag}
className="primary-button rounded-full px-2 py-0.5 text-xs text-primary-cta-text"
>
{itemTag}
</span>
))}
</div>
</div>
</motion.a>
))}
</div>
</div>
</section>
);
};
export default BlogTagCards;

View File

@@ -0,0 +1,82 @@
import { motion } from "motion/react";
const ContactCenter = ({
tag,
title,
description,
inputPlaceholder,
buttonText,
termsText,
}: {
tag: string;
title: string;
description: string;
inputPlaceholder: string;
buttonText: string;
termsText?: string;
}) => {
return (
<section
data-webild-section="ContactCenter"
aria-label="Contact section"
className="relative w-full py-20"
>
<div className="w-content-width mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex items-center justify-center py-20 card rounded-theme"
>
<div className="flex flex-col items-center w-full md:w-1/2 gap-3 px-5">
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-5xl md:text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-8/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
<form className="flex flex-col md:flex-row w-full md:w-8/10 2xl:w-6/10 gap-3 md:gap-1 p-1 mt-2 card rounded-theme">
<input
type="email"
placeholder={inputPlaceholder}
aria-label="Email address"
className="flex-1 px-5 py-3 md:py-0 text-base text-center md:text-left bg-transparent placeholder:opacity-75 focus:outline-none truncate"
/>
<button
type="submit"
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{buttonText}
</button>
</form>
{termsText && (
<p className="text-xs opacity-75 text-center md:max-w-8/10 2xl:max-w-6/10">
{termsText}
</p>
)}
</div>
</motion.div>
</div>
</section>
);
};
export default ContactCenter;

View File

@@ -0,0 +1,70 @@
import { motion } from "motion/react";
const ContactCta = ({
tag,
text,
primaryButton,
secondaryButton,
}: {
tag: string;
text: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
}) => {
return (
<section
data-webild-section="ContactCta"
aria-label="Contact section"
className="relative w-full py-20"
>
<div className="w-content-width mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex items-center justify-center py-20 px-5 md:px-8 card rounded-theme"
>
<div className="w-full md:w-3/4 flex flex-col items-center gap-3">
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-4xl md:text-5xl font-medium text-center leading-tight text-balance"
>
{text}
</motion.h2>
<div className="flex flex-wrap justify-center gap-3 mt-1">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</motion.div>
</div>
</section>
);
};
export default ContactCta;

View File

@@ -0,0 +1,108 @@
import { motion } from "motion/react";
type ContactSplitEmailProps = {
tag: string;
title: string;
description: string;
inputPlaceholder: string;
buttonText: string;
termsText?: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const ContactSplitEmail = ({
tag,
title,
description,
inputPlaceholder,
buttonText,
termsText,
imageSrc,
videoSrc,
}: ContactSplitEmailProps) => {
return (
<section
data-webild-section="ContactSplitEmail"
aria-label="Contact section"
className="relative w-full py-20"
>
<div className="w-content-width mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-2 gap-5"
>
<div className="flex items-center justify-center py-15 md:py-20 card rounded-theme">
<div className="flex flex-col items-center w-full gap-3 px-5">
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-5xl md:text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-8/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
<form className="flex flex-col md:flex-row w-full md:w-8/10 2xl:w-6/10 gap-3 md:gap-1 p-1 mt-2 card rounded-theme">
<input
type="email"
placeholder={inputPlaceholder}
aria-label="Email address"
className="flex-1 px-5 py-3 md:py-0 text-base text-center md:text-left bg-transparent placeholder:opacity-75 focus:outline-none truncate"
/>
<button
type="submit"
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{buttonText}
</button>
</form>
{termsText && (
<p className="text-xs opacity-75 text-center md:max-w-8/10 2xl:max-w-6/10">
{termsText}
</p>
)}
</div>
</div>
<div className="h-100 md:h-auto card rounded-theme overflow-hidden">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Contact video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
</motion.div>
</div>
</section>
);
};
export default ContactSplitEmail;

View File

@@ -0,0 +1,135 @@
import { motion } from "motion/react";
type InputField = {
name: string;
type: string;
placeholder: string;
required?: boolean;
};
type TextareaField = {
name: string;
placeholder: string;
rows?: number;
required?: boolean;
};
type ContactSplitFormProps = {
tag: string;
title: string;
description: string;
inputs: InputField[];
textarea?: TextareaField;
buttonText: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const ContactSplitForm = ({
tag,
title,
description,
inputs,
textarea,
buttonText,
imageSrc,
videoSrc,
}: ContactSplitFormProps) => {
return (
<section
data-webild-section="ContactSplitForm"
aria-label="Contact section"
className="relative w-full py-20"
>
<div className="w-content-width mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-2 md:auto-rows-fr gap-5"
>
<div className="p-5 md:p-8 card rounded-theme">
<form className="flex flex-col gap-5">
<div className="flex flex-col items-center gap-1 text-center">
<span className="card rounded-full px-3 py-1 text-sm">{tag}</span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-4xl font-medium text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-sm md:text-base leading-tight text-balance"
>
{description}
</motion.p>
</div>
<div className="flex flex-col gap-3">
{inputs.map((input) => (
<input
key={input.name}
type={input.type}
name={input.name}
placeholder={input.placeholder}
required={input.required}
aria-label={input.placeholder}
className="card rounded-theme h-9 px-3 w-full text-sm"
/>
))}
{textarea && (
<textarea
name={textarea.name}
placeholder={textarea.placeholder}
required={textarea.required}
rows={textarea.rows || 5}
aria-label={textarea.placeholder}
className="card rounded-theme p-3 w-full text-sm"
/>
)}
<button
type="submit"
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{buttonText}
</button>
</div>
</form>
</div>
<div className="h-100 md:h-full card rounded-theme overflow-hidden">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Contact video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
</motion.div>
</div>
</section>
);
};
export default ContactSplitForm;

View File

@@ -0,0 +1,107 @@
import { motion } from "motion/react";
import { ChevronDown } from "lucide-react";
type FaqItem = {
question: string;
answer: string;
};
const FaqSimple = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FaqItem[];
}) => {
return (
<section
data-webild-section="FaqSimple"
aria-label="FAQ section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="flex flex-col gap-3">
{items.map((item, index) => (
<motion.details
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.05 + index * 0.05, ease: "easeOut" }}
className="card rounded-theme p-4 group"
>
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
</summary>
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
</motion.details>
))}
</div>
</div>
</section>
);
};
export default FaqSimple;

View File

@@ -0,0 +1,139 @@
import { motion } from "motion/react";
import { ChevronDown } from "lucide-react";
type FaqItem = {
question: string;
answer: string;
};
type FaqSplitMediaProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FaqItem[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const FaqSplitMedia = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
imageSrc,
videoSrc,
}: FaqSplitMediaProps) => {
return (
<section
data-webild-section="FaqSplitMedia"
aria-label="FAQ section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-5">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card relative md:col-span-2 h-80 md:h-auto rounded-theme overflow-hidden"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="FAQ video"
className="absolute inset-0 size-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="absolute inset-0 size-full object-cover"
/>
)}
</motion.div>
<div className="md:col-span-3 flex flex-col gap-3">
{items.map((item, index) => (
<motion.details
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="card rounded-theme p-4 group"
>
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
</summary>
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
</motion.details>
))}
</div>
</div>
</div>
</section>
);
};
export default FaqSplitMedia;

View File

@@ -0,0 +1,107 @@
import { motion } from "motion/react";
import { ChevronDown } from "lucide-react";
type FaqItem = {
question: string;
answer: string;
};
const FaqTwoColumn = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FaqItem[];
}) => {
return (
<section
data-webild-section="FaqTwoColumn"
aria-label="FAQ section"
className="relative w-full py-16 md:py-24"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-5">
{items.map((item, index) => (
<motion.details
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.05 + index * 0.05, ease: "easeOut" }}
className="card rounded-theme p-4 group"
>
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none">
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
<ChevronDown className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180" strokeWidth={2} />
</summary>
<p className="mt-2 text-sm leading-tight text-muted-foreground">{item.answer}</p>
</motion.details>
))}
</div>
</div>
</section>
);
};
export default FaqTwoColumn;

View File

@@ -0,0 +1,159 @@
import { motion } from "motion/react";
type FeatureItem = {
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesAlternatingSplitProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesAlternatingSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesAlternatingSplitProps) => {
return (
<section
data-webild-section="FeaturesAlternatingSplit"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="flex flex-col gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className={`flex flex-col gap-5 md:gap-8 p-5 md:p-8 card rounded-theme ${index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse"}`}
>
<div className="flex flex-col justify-center w-full md:w-1/2 gap-3">
<span className="flex items-center justify-center size-8 mb-1 text-sm rounded-theme primary-button text-primary-cta-text">
{index + 1}
</span>
<h3 className="text-4xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
<p className="text-base leading-tight text-balance">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap gap-3 mt-2">
{item.primaryButton && (
<a
href={item.primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{item.primaryButton.text}
</a>
)}
{item.secondaryButton && (
<a
href={item.secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{item.secondaryButton.text}
</a>
)}
</div>
)}
</div>
<div className="w-full md:w-1/2 aspect-square rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesAlternatingSplit;

View File

@@ -0,0 +1,142 @@
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
type FeatureItem = {
title: string;
tags: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesArrowCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesArrowCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesArrowCardsProps) => {
return (
<section
data-webild-section="FeaturesArrowCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="flex flex-col gap-5 h-full cursor-pointer group"
>
<div className="aspect-square rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
)}
</div>
<div className="flex flex-col justify-between gap-3 p-4 md:p-5 flex-1 card rounded-theme">
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
<div className="flex items-center justify-between gap-5">
<div className="flex flex-wrap items-center gap-2">
{item.tags.map((itemTag) => (
<span key={itemTag} className="card rounded-full px-3 py-1 text-sm">{itemTag}</span>
))}
</div>
<ArrowRight className="shrink-0 h-4 w-auto transition-transform duration-300 group-hover:-rotate-45" strokeWidth={1.5} />
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesArrowCards;

View File

@@ -0,0 +1,306 @@
import { motion } from "motion/react";
import { Check } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import Marquee from "@/components/ui/marquee";
type FeatureCard = { title: string; description: string } & (
| { bentoComponent: "info-card-marquee"; infoCards: { icon: LucideIcon; label: string; value: string }[] }
| { bentoComponent: "tilted-stack-cards"; stackCards: [{ icon: LucideIcon; title: string; subtitle: string; detail: string }, { icon: LucideIcon; title: string; subtitle: string; detail: string }, { icon: LucideIcon; title: string; subtitle: string; detail: string }] }
| { bentoComponent: "animated-bar-chart" }
| { bentoComponent: "orbiting-icons"; centerIcon: LucideIcon; orbitIcons: LucideIcon[] }
| { bentoComponent: "icon-text-marquee"; centerIcon: LucideIcon; marqueeTexts: string[] }
| { bentoComponent: "chat-marquee"; aiIcon: LucideIcon; userIcon: LucideIcon; exchanges: { userMessage: string; aiResponse: string }[]; placeholder: string }
| { bentoComponent: "checklist-timeline"; heading: string; subheading: string; checklistItems: [{ label: string; detail: string }, { label: string; detail: string }, { label: string; detail: string }]; completedLabel: string }
| { bentoComponent: "media-stack"; mediaItems: [{ imageSrc?: string; videoSrc?: string }, { imageSrc?: string; videoSrc?: string }, { imageSrc?: string; videoSrc?: string }] }
);
interface FeaturesBentoProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
features: FeatureCard[];
}
const FeaturesBento = ({
tag,
title,
description,
primaryButton,
secondaryButton,
features,
}: FeaturesBentoProps) => {
return (
<section
data-webild-section="FeaturesBento"
aria-label="Features bento section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-6 auto-rows-fr gap-5 w-content-width mx-auto">
{features.map((feature, index) => {
const spanClass =
index % 5 === 0 ? "md:col-span-4" :
index % 5 === 1 ? "md:col-span-2" :
index % 5 === 2 ? "md:col-span-3" :
index % 5 === 3 ? "md:col-span-3" :
"md:col-span-6";
return (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className={`flex flex-col gap-4 p-4 md:p-5 card rounded-theme h-full ${spanClass}`}
>
<div className="relative h-72 overflow-hidden rounded-theme">
{feature.bentoComponent === "info-card-marquee" && (
<div className="absolute inset-0 flex flex-col gap-2 p-4 overflow-hidden">
<Marquee speed={30}>
{feature.infoCards.map((c) => {
const Icon = c.icon;
return (
<div key={c.label} className="flex items-center gap-3 card rounded-theme px-4 py-3">
<Icon className="size-5 shrink-0" strokeWidth={1.5} />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">{c.label}</span>
<span className="text-base font-medium">{c.value}</span>
</div>
</div>
);
})}
</Marquee>
</div>
)}
{feature.bentoComponent === "tilted-stack-cards" && (
<div className="absolute inset-0 flex items-center justify-center">
{feature.stackCards.map((card, i) => {
const Icon = card.icon;
const rotate = (i - 1) * 6;
const translate = (i - 1) * 8;
return (
<div
key={card.title}
style={{ transform: `rotate(${rotate}deg) translateX(${translate}px)`, zIndex: i }}
className="absolute w-48 card rounded-theme p-4 flex flex-col gap-2"
>
<Icon className="size-5" strokeWidth={1.5} />
<h4 className="text-sm font-medium leading-tight">{card.title}</h4>
<p className="text-xs leading-tight">{card.subtitle}</p>
<p className="text-2xs text-muted-foreground">{card.detail}</p>
</div>
);
})}
</div>
)}
{feature.bentoComponent === "animated-bar-chart" && (
<div className="absolute inset-0 flex items-end justify-around gap-2 p-4">
{[40, 65, 85, 55, 75, 95, 60].map((h, i) => (
<motion.div
key={i}
initial={{ height: 0 }}
whileInView={{ height: `${h}%` }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: i * 0.1, ease: "easeOut" }}
className="w-full bg-foreground/80 rounded-theme"
/>
))}
</div>
)}
{feature.bentoComponent === "orbiting-icons" && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative size-48">
<div className="absolute inset-0 rounded-full border border-foreground/10" />
<div className="absolute inset-4 rounded-full border border-foreground/10" />
<div className="absolute inset-1/2 -translate-x-1/2 -translate-y-1/2 size-12 card rounded-full flex items-center justify-center">
<feature.centerIcon className="size-5" strokeWidth={1.5} />
</div>
{feature.orbitIcons.slice(0, 6).map((Icon, i) => {
const angle = (i / Math.min(feature.orbitIcons.length, 6)) * Math.PI * 2;
const r = 80;
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r;
return (
<div
key={i}
style={{ left: `calc(50% + ${x}px)`, top: `calc(50% + ${y}px)` }}
className="absolute -translate-x-1/2 -translate-y-1/2 size-8 card rounded-full flex items-center justify-center"
>
<Icon className="size-4" strokeWidth={1.5} />
</div>
);
})}
</div>
</div>
)}
{feature.bentoComponent === "icon-text-marquee" && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3">
<div className="size-12 card rounded-full flex items-center justify-center">
<feature.centerIcon className="size-5" strokeWidth={1.5} />
</div>
<Marquee speed={25} className="w-full">
{feature.marqueeTexts.map((t, i) => (
<span key={i} className="card rounded-full px-4 py-2 text-sm whitespace-nowrap">{t}</span>
))}
</Marquee>
</div>
)}
{feature.bentoComponent === "chat-marquee" && (
<div className="absolute inset-0 flex flex-col gap-2 p-4 overflow-hidden">
{feature.exchanges.slice(0, 3).map((ex, i) => {
const UserIcon = feature.userIcon;
const AiIcon = feature.aiIcon;
return (
<div key={i} className="flex flex-col gap-2">
<div className="flex items-start gap-2 self-end max-w-7/10">
<span className="card rounded-theme px-3 py-2 text-xs">{ex.userMessage}</span>
<UserIcon className="size-5 shrink-0" strokeWidth={1.5} />
</div>
<div className="flex items-start gap-2 self-start max-w-7/10">
<AiIcon className="size-5 shrink-0" strokeWidth={1.5} />
<span className="card rounded-theme px-3 py-2 text-xs">{ex.aiResponse}</span>
</div>
</div>
);
})}
<div className="mt-auto card rounded-theme px-3 py-2 text-xs text-muted-foreground">{feature.placeholder}</div>
</div>
)}
{feature.bentoComponent === "checklist-timeline" && (
<div className="absolute inset-0 flex flex-col gap-3 p-4">
<h4 className="text-sm font-medium">{feature.heading}</h4>
<p className="text-xs text-muted-foreground">{feature.subheading}</p>
<div className="flex flex-col gap-2 mt-2">
{feature.checklistItems.map((it) => (
<div key={it.label} className="flex items-start gap-2">
<div className="size-5 rounded-full primary-button flex items-center justify-center shrink-0">
<Check className="size-3 text-primary-cta-text" strokeWidth={2} />
</div>
<div className="flex flex-col">
<span className="text-xs font-medium">{it.label}</span>
<span className="text-2xs text-muted-foreground">{it.detail}</span>
</div>
</div>
))}
</div>
<span className="mt-auto text-xs font-medium">{feature.completedLabel}</span>
</div>
)}
{feature.bentoComponent === "media-stack" && (
<div className="absolute inset-0 flex items-center justify-center">
{feature.mediaItems.map((m, i) => {
const rotate = (i - 1) * 6;
const translate = (i - 1) * 12;
return (
<div
key={i}
style={{ transform: `rotate(${rotate}deg) translateX(${translate}px)`, zIndex: i }}
className="absolute w-40 aspect-square card rounded-theme overflow-hidden p-2"
>
{m.videoSrc ? (
<video
src={m.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={m.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
);
})}
</div>
)}
</div>
<div className="flex flex-col gap-1">
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
<p className="text-sm leading-tight">{feature.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default FeaturesBento;

View File

@@ -0,0 +1,123 @@
import { motion } from "motion/react";
import type { LucideIcon } from "lucide-react";
type FeatureItem = {
icon: LucideIcon;
title: string;
description: string;
};
interface FeaturesBorderGlowProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
features: FeatureItem[];
}
const FeaturesBorderGlow = ({
tag,
title,
description,
primaryButton,
secondaryButton,
features,
}: FeaturesBorderGlowProps) => {
return (
<section
data-webild-section="FeaturesBorderGlow"
aria-label="Features border glow section"
className="relative w-full py-20"
>
<div className="flex flex-col w-content-width mx-auto gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{features.map((feature, index) => {
const FeatureIcon = feature.icon;
return (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="relative flex flex-col justify-between gap-4 p-4 md:p-5 h-full min-h-60 card rounded-theme"
>
<div className="flex items-center justify-center size-12 primary-button rounded-theme">
<FeatureIcon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
<p className="text-sm leading-tight">{feature.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default FeaturesBorderGlow;

View File

@@ -0,0 +1,121 @@
import { motion } from "motion/react";
import { Check, X } from "lucide-react";
interface FeaturesComparisonProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
negativeItems: string[];
positiveItems: string[];
}
const FeaturesComparison = ({
tag,
title,
description,
primaryButton,
secondaryButton,
negativeItems,
positiveItems,
}: FeaturesComparisonProps) => {
return (
<section
data-webild-section="FeaturesComparison"
aria-label="Features comparison section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-2 w-content-width md:w-6/10 mx-auto gap-5"
>
<div className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme opacity-50">
{negativeItems.map((item) => (
<div key={item} className="flex items-center gap-2 text-base">
<X className="shrink-0 size-4" />
<span className="text-base truncate">{item}</span>
</div>
))}
</div>
<div className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme">
{positiveItems.map((item) => (
<div key={item} className="flex items-center gap-2 text-base">
<Check className="shrink-0 size-4" />
<span className="text-base truncate">{item}</span>
</div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default FeaturesComparison;

View File

@@ -0,0 +1,147 @@
import { motion } from "motion/react";
type FeatureItem = {
title: string;
description: string;
tags: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesDetailedCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesDetailedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesDetailedCardsProps) => {
return (
<section
data-webild-section="FeaturesDetailedCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="flex flex-col w-content-width mx-auto gap-5">
{items.map((item) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="flex flex-col md:grid md:grid-cols-10 2xl:w-8/10 mx-auto gap-5 md:gap-8 p-5 md:p-8 cursor-pointer card rounded-theme group"
>
<div className="flex flex-col md:col-span-6 gap-3 md:gap-8">
<h3 className="text-3xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
<div className="flex flex-col mt-auto gap-5">
<div className="flex flex-wrap gap-2">
{item.tags.map((itemTag) => (
<span key={itemTag} className="card rounded-full px-3 py-1 text-sm">{itemTag}</span>
))}
</div>
<p className="text-base md:text-2xl leading-tight text-balance">
{item.description}
</p>
</div>
</div>
<div className="aspect-square md:col-span-4 rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesDetailedCards;

View File

@@ -0,0 +1,150 @@
import { motion } from "motion/react";
type StepItem = {
tag: string;
title: string;
subtitle: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesDetailedStepsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
steps: StepItem[];
}
const FeaturesDetailedSteps = ({
tag,
title,
description,
primaryButton,
secondaryButton,
steps,
}: FeaturesDetailedStepsProps) => {
return (
<section
data-webild-section="FeaturesDetailedSteps"
aria-label="Features detailed steps section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="flex flex-col w-content-width mx-auto gap-5">
{steps.map((step, index) => {
const stepNumber = String(index + 1).padStart(2, "0");
const tilt = index % 2 === 0 ? "rotate-3" : "-rotate-3";
return (
<motion.div
key={step.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="flex flex-col md:flex-row justify-between 2xl:w-8/10 mx-auto gap-5 md:gap-8 p-5 md:p-8 card rounded-theme overflow-hidden"
>
<div className="flex flex-col justify-between w-full md:w-1/2">
<div className="flex flex-col gap-5">
<span className="w-fit card rounded-full px-3 py-1 mb-1 text-sm">{step.tag}</span>
<h3 className="text-5xl md:text-8xl font-medium leading-none">{step.title}</h3>
</div>
<div className="block md:hidden w-full h-px my-5 bg-foreground/20" />
<div className="flex flex-col gap-2 md:gap-5">
<h4 className="text-2xl md:text-3xl font-medium text-balance">{step.subtitle}</h4>
<p className="text-base md:text-lg leading-tight text-balance">{step.description}</p>
</div>
</div>
<div className="flex flex-col w-full md:w-35/100 gap-8">
<span className="hidden md:block self-end text-8xl font-medium">{stepNumber}</span>
<div className={`aspect-square rounded-theme overflow-hidden ${tilt}`}>
{step.videoSrc ? (
<video
src={step.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Step video"
className="w-full h-full object-cover"
/>
) : (
<img
src={step.imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default FeaturesDetailedSteps;

View File

@@ -0,0 +1,142 @@
import { motion } from "motion/react";
import { Plus } from "lucide-react";
type FeatureItem = {
title: string;
descriptions: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesFlipCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesFlipCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesFlipCardsProps) => {
return (
<section
data-webild-section="FeaturesFlipCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="group flex flex-col gap-4 p-4 md:p-5 card rounded-theme"
>
<div className="flex items-start justify-between gap-5">
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
<div className="flex items-center justify-center shrink-0 size-8 primary-button rounded-theme transition-transform duration-300 group-hover:rotate-45">
<Plus className="size-4 text-primary-cta-text" />
</div>
</div>
<div className="relative overflow-hidden aspect-4/5 rounded-theme">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
)}
</div>
<div className="flex flex-col gap-2">
{item.descriptions.map((desc, i) => (
<p key={i} className="text-base leading-tight">{desc}</p>
))}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesFlipCards;

View File

@@ -0,0 +1,126 @@
import { motion } from "motion/react";
import type { LucideIcon } from "lucide-react";
import HoverPattern from "@/components/ui/hover-pattern";
type FeatureItem = {
icon: LucideIcon;
title: string;
description: string;
};
interface FeaturesIconCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
features: FeatureItem[];
}
const FeaturesIconCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
features,
}: FeaturesIconCardsProps) => {
return (
<section
data-webild-section="FeaturesIconCards"
aria-label="Features icon cards section"
className="relative w-full py-20"
>
<div className="flex flex-col w-content-width mx-auto gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{features.map((feature, index) => {
const FeatureIcon = feature.icon;
return (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="flex flex-col gap-4 p-4 md:p-5 card rounded-theme"
>
<HoverPattern className="flex items-center justify-center aspect-square rounded-theme">
<div className="relative z-10 flex items-center justify-center size-12 primary-button rounded-theme">
<FeatureIcon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
</div>
</HoverPattern>
<div className="flex flex-col gap-1">
<h3 className="text-2xl font-medium leading-tight">{feature.title}</h3>
<p className="text-sm leading-tight">{feature.description}</p>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default FeaturesIconCards;

View File

@@ -0,0 +1,147 @@
import { motion } from "motion/react";
type FeatureItem = {
label: string;
title: string;
bullets: string[];
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
};
interface FeaturesLabeledListProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesLabeledList = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesLabeledListProps) => {
return (
<section
data-webild-section="FeaturesLabeledList"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="flex flex-col gap-5 w-content-width mx-auto">
{items.map((item) => (
<motion.div
key={item.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="flex flex-col md:flex-row gap-5 md:gap-8 p-5 md:p-8 card rounded-theme"
>
<div className="w-full md:w-1/2 flex md:justify-start">
<h3 className="text-7xl font-medium leading-tight truncate">{item.label}</h3>
</div>
<div className="w-full h-px bg-foreground/20 md:hidden" />
<div className="flex flex-col w-full md:w-1/2 gap-5">
<h4 className="text-2xl md:text-3xl font-medium leading-tight">{item.title}</h4>
<div className="flex flex-wrap items-center gap-1">
{item.bullets.map((bulletText, i) => (
<span key={i} className="flex items-center gap-1">
<span className="text-base">{bulletText}</span>
{i < item.bullets.length - 1 && <span className="text-base"></span>}
</span>
))}
</div>
<div className="flex flex-wrap gap-3 mt-2">
<a
href={item.primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{item.primaryButton.text}
</a>
<a
href={item.secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{item.secondaryButton.text}
</a>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesLabeledList;

View File

@@ -0,0 +1,134 @@
import { motion } from "motion/react";
type FeatureItem = {
title: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesMediaCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesMediaCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesMediaCardsProps) => {
return (
<section
data-webild-section="FeaturesMediaCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="flex flex-col gap-4 p-4 md:p-5 h-full card rounded-theme"
>
<div className="aspect-square rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
<div className="flex flex-col gap-1">
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
<p className="text-sm leading-tight">{item.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesMediaCards;

View File

@@ -0,0 +1,143 @@
import { motion } from "motion/react";
import type { LucideIcon } from "lucide-react";
import Marquee from "@/components/ui/marquee";
type FeatureItem = {
title: string;
description: string;
buttonIcon: LucideIcon;
buttonHref?: string;
buttonOnClick?: () => void;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesMediaCarouselProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesMediaCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesMediaCarouselProps) => {
return (
<section
data-webild-section="FeaturesMediaCarousel"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col w-full gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<Marquee speed={50} pauseOnHover>
{items.map((item) => {
const Icon = item.buttonIcon;
return (
<div key={item.title} className="relative overflow-hidden w-90 aspect-square md:aspect-3/2 rounded-theme shrink-0">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-foreground/60 to-transparent" />
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-5 p-5">
<div className="flex flex-col min-w-0">
<h3 className="text-2xl md:text-3xl font-medium leading-tight text-background">{item.title}</h3>
<p className="text-sm md:text-base leading-tight text-background/75">{item.description}</p>
</div>
<a
href={item.buttonHref ?? "#"}
aria-label={item.buttonHref ? `Navigate to ${item.buttonHref}` : "Action button"}
className="flex items-center justify-center shrink-0 size-8 primary-button rounded-theme"
>
<Icon className="size-4 text-primary-cta-text" strokeWidth={1.5} />
</a>
</div>
</div>
);
})}
</Marquee>
</div>
</section>
);
};
export default FeaturesMediaCarousel;

View File

@@ -0,0 +1,163 @@
import { motion } from "motion/react";
import { BadgeCheck } from "lucide-react";
type FeatureItem = {
title: string;
description: string;
avatarSrc: string;
buttonText: string;
buttonHref?: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesProfileCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesProfileCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesProfileCardsProps) => {
return (
<section
data-webild-section="FeaturesProfileCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="group relative overflow-hidden aspect-5/6 rounded-theme"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Profile video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<a
href={item.buttonHref ?? "#"}
className="absolute top-5 right-5 z-20 primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{item.buttonText}
</a>
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-linear-to-t from-foreground/80 to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-10 p-3">
<div className="relative flex flex-col gap-1 p-3">
<div className="absolute inset-0 -z-10 card rounded-theme translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
<div className="flex items-center gap-2">
<div className="size-8 shrink-0 overflow-hidden rounded-full secondary-button">
<img src={item.avatarSrc} alt="" className="h-full w-full object-cover" />
</div>
<h3 className="text-xl font-semibold leading-tight truncate text-background transition-colors duration-400 group-hover:text-foreground">
{item.title}
</h3>
<BadgeCheck className="size-5 shrink-0 text-background transition-colors duration-400 group-hover:text-foreground" strokeWidth={2} />
</div>
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
{item.description}
</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesProfileCards;

View File

@@ -0,0 +1,153 @@
import { motion } from "motion/react";
import { Info } from "lucide-react";
type FeatureItem = {
title: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesRevealCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesRevealCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesRevealCardsProps) => {
return (
<section
data-webild-section="FeaturesRevealCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="group relative overflow-hidden aspect-6/7 rounded-theme"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div className="absolute top-5 left-5 z-20">
<div className="relative size-8 rounded-full bg-background flex items-center justify-center">
<span className="text-sm font-medium text-foreground transition-opacity duration-300 group-hover:opacity-0">{index + 1}</span>
<Info className="absolute size-4 text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100" strokeWidth={1.5} />
</div>
</div>
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-linear-to-t from-foreground/80 to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-10 p-3">
<div className="relative flex flex-col gap-1 p-3">
<div className="absolute inset-0 -z-10 card rounded-theme translate-y-full opacity-0 transition-all duration-400 ease-out group-hover:translate-y-0 group-hover:opacity-100" />
<h3 className="text-2xl font-semibold leading-tight text-background transition-colors duration-400 group-hover:text-foreground">
{item.title}
</h3>
<div className="grid grid-rows-[0fr] transition-all duration-400 ease-out group-hover:grid-rows-[1fr]">
<p className="overflow-hidden text-sm leading-tight text-foreground opacity-0 transition-opacity duration-400 group-hover:opacity-100">
{item.description}
</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesRevealCards;

View File

@@ -0,0 +1,127 @@
import { motion } from "motion/react";
type FeatureItem = {
title: string;
description: string;
label: string;
value: string;
};
interface FeaturesStatisticsCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesStatisticsCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesStatisticsCardsProps) => {
return (
<section
data-webild-section="FeaturesStatisticsCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="flex flex-col h-full card rounded-theme"
>
<div className="flex flex-col flex-1 gap-8 p-5">
<div className="flex flex-col gap-1">
<h3 className="text-2xl md:text-3xl font-medium leading-tight truncate">{item.title}</h3>
<p className="text-base md:text-lg leading-tight">{item.description}</p>
</div>
<div className="flex items-center justify-between gap-2 mt-auto">
<div className="flex items-center min-w-0 flex-1 gap-2">
<span className="shrink-0 size-3 rounded-full bg-foreground" />
<span className="text-base truncate">{item.label}</span>
</div>
<span className="text-xl md:text-2xl font-medium">{item.value}</span>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesStatisticsCards;

View File

@@ -0,0 +1,158 @@
import { motion } from "motion/react";
type FeatureItem = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesTaggedCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesTaggedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesTaggedCardsProps) => {
return (
<section
data-webild-section="FeaturesTaggedCards"
aria-label="Features section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="flex flex-col gap-5 h-full group"
>
<div className="relative aspect-square rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Feature video"
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105"
/>
)}
<span className="absolute top-5 right-5 card rounded-full px-3 py-1 text-sm">{item.tag}</span>
</div>
<div className="flex flex-col gap-4 p-4 md:p-5 flex-1 card rounded-theme">
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
<p className="text-base leading-tight">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap gap-3 mt-2">
{item.primaryButton && (
<a
href={item.primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{item.primaryButton.text}
</a>
)}
{item.secondaryButton && (
<a
href={item.secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{item.secondaryButton.text}
</a>
)}
</div>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesTaggedCards;

View File

@@ -0,0 +1,141 @@
import { motion } from "motion/react";
type FeatureItem = {
title: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesTimelineCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesTimelineCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesTimelineCardsProps) => {
return (
<section
data-webild-section="FeaturesTimelineCards"
aria-label="Features timeline section"
className="relative w-full py-20"
>
<div className="flex flex-col w-content-width mx-auto gap-8">
<div className="flex flex-col items-center gap-2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
)}
{secondaryButton && (
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
)}
</div>
)}
</div>
<div className="relative flex flex-col gap-8">
<div className="absolute left-4 top-2 bottom-2 w-px bg-foreground/20" aria-hidden="true" />
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.1 + index * 0.05, ease: "easeOut" }}
className="relative flex gap-5 pl-8"
>
<div className="absolute left-2 top-2 size-5 rounded-full primary-button flex items-center justify-center">
<span className="text-2xs font-medium text-primary-cta-text">{index + 1}</span>
</div>
<div className="flex flex-col md:flex-row gap-5 md:gap-8 flex-1 p-4 md:p-5 card rounded-theme">
<div className="flex flex-col gap-2 w-full md:w-1/2">
<h3 className="text-3xl font-medium leading-tight text-balance">{item.title}</h3>
<p className="text-base leading-tight text-balance">{item.description}</p>
</div>
<div className="w-full md:w-1/2 aspect-video rounded-theme overflow-hidden">
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Timeline video"
className="w-full h-full object-cover"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesTimelineCards;

View File

@@ -0,0 +1,56 @@
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
title: string;
items: FooterLink[];
};
const FooterBasic = ({
columns,
leftText,
rightText,
}: {
columns: FooterColumn[];
leftText: string;
rightText: string;
}) => {
return (
<footer
data-webild-section="FooterBasic"
aria-label="Site footer"
className="w-full pt-20 pb-8 border-t border-foreground/15"
>
<div className="w-content-width mx-auto">
<div className="w-full flex flex-wrap justify-between gap-y-8 mb-8">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<a
key={item.label}
href={item.href}
className="text-base hover:opacity-75 transition-opacity"
>
{item.label}
</a>
))}
</div>
))}
</div>
<div className="w-full h-px bg-foreground/20" />
<div className="w-full flex items-center justify-between pt-5">
<span className="text-sm opacity-50">{leftText}</span>
<span className="text-sm opacity-50">{rightText}</span>
</div>
</div>
</footer>
);
};
export default FooterBasic;

View File

@@ -0,0 +1,51 @@
import { ChevronRight } from "lucide-react";
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
items: FooterLink[];
};
const FooterBrand = ({
brand,
columns,
}: {
brand: string;
columns: FooterColumn[];
}) => {
return (
<footer
data-webild-section="FooterBrand"
aria-label="Site footer"
className="w-full py-8 mt-20 overflow-hidden primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto flex flex-col gap-8 md:gap-8">
<h2 className="text-7xl md:text-9xl font-medium leading-none">{brand}</h2>
<div className="flex flex-col gap-8 mb-8 md:flex-row md:justify-between">
{columns.map((column, index) => (
<div key={index} className="flex flex-col items-start gap-3">
{column.items.map((item) => (
<div key={item.label} className="flex items-center gap-2 text-base">
<ChevronRight className="w-4 h-4" strokeWidth={3} aria-hidden="true" />
<a
href={item.href}
className="text-base text-primary-cta-text font-medium hover:opacity-75 transition-opacity"
>
{item.label}
</a>
</div>
))}
</div>
))}
</div>
</div>
</footer>
);
};
export default FooterBrand;

View File

@@ -0,0 +1,51 @@
import { ChevronRight } from "lucide-react";
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
items: FooterLink[];
};
const FooterBrandReveal = ({
brand,
columns,
}: {
brand: string;
columns: FooterColumn[];
}) => {
return (
<footer
data-webild-section="FooterBrandReveal"
aria-label="Site footer"
className="w-full py-8 mt-20 overflow-hidden primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto flex flex-col gap-8">
<h2 className="text-7xl md:text-9xl font-medium leading-none">{brand}</h2>
<div className="flex flex-col gap-8 mb-8 md:flex-row md:justify-between">
{columns.map((column, index) => (
<div key={index} className="flex flex-col items-start gap-3">
{column.items.map((item) => (
<div key={item.label} className="flex items-center gap-2 text-base">
<ChevronRight className="w-4 h-4" strokeWidth={3} aria-hidden="true" />
<a
href={item.href}
className="text-base text-primary-cta-text font-medium hover:opacity-75 transition-opacity"
>
{item.label}
</a>
</div>
))}
</div>
))}
</div>
</div>
</footer>
);
};
export default FooterBrandReveal;

View File

@@ -0,0 +1,53 @@
import type { LucideIcon } from "lucide-react";
type SocialLink = {
icon: string | LucideIcon;
href?: string;
onClick?: () => void;
};
const FooterMinimal = ({
brand,
copyright,
socialLinks,
}: {
brand: string;
copyright: string;
socialLinks?: SocialLink[];
}) => {
return (
<footer
data-webild-section="FooterMinimal"
aria-label="Site footer"
className="relative w-full py-20"
>
<div className="flex flex-col w-content-width mx-auto px-8 pb-5 rounded-theme card">
<h2 className="text-7xl md:text-9xl font-medium leading-none py-5">{brand}</h2>
<div className="h-px w-full mb-5 bg-foreground/50" />
<div className="flex flex-col gap-3 items-center justify-between md:flex-row">
<span className="text-base opacity-75">{copyright}</span>
{socialLinks && socialLinks.length > 0 && (
<div className="flex items-center gap-3">
{socialLinks.map((link, index) => {
const Icon = typeof link.icon === "string" ? null : link.icon;
return (
<a
key={index}
href={link.href}
className="flex items-center justify-center size-8 rounded-full primary-button text-primary-cta-text"
>
{Icon && <Icon className="w-4 h-4" strokeWidth={1.5} />}
</a>
);
})}
</div>
)}
</div>
</div>
</footer>
);
};
export default FooterMinimal;

View File

@@ -0,0 +1,70 @@
type FooterColumn = {
title: string;
items: { label: string; href?: string; onClick?: () => void }[];
};
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
const FooterSimple = ({
brand,
columns,
copyright,
links,
}: {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
}) => {
return (
<footer
data-webild-section="FooterSimple"
aria-label="Site footer"
className="w-full py-8 mt-20 primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
<h2 className="text-4xl font-medium">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<a
key={item.label}
href={item.href}
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
>
{item.label}
</a>
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<a
key={link.label}
href={link.href}
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</footer>
);
};
export default FooterSimple;

View File

@@ -0,0 +1,70 @@
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
title: string;
items: FooterLink[];
};
const FooterSimpleCard = ({
brand,
columns,
copyright,
links,
}: {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
}) => {
return (
<footer
data-webild-section="FooterSimpleCard"
aria-label="Site footer"
className="w-full py-20"
>
<div className="w-content-width mx-auto p-8 rounded-theme card">
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
<h2 className="text-4xl font-medium">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<a
key={item.label}
href={item.href}
className="text-base hover:opacity-75 transition-opacity"
>
{item.label}
</a>
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-foreground/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<a
key={link.label}
href={link.href}
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</footer>
);
};
export default FooterSimpleCard;

View File

@@ -0,0 +1,96 @@
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
title: string;
items: FooterLink[];
};
type FooterSimpleMediaProps = ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }) & {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
};
const FooterSimpleMedia = ({
imageSrc,
videoSrc,
brand,
columns,
copyright,
links,
}: FooterSimpleMediaProps) => {
return (
<footer
data-webild-section="FooterSimpleMedia"
aria-label="Site footer"
className="relative w-full mt-20 overflow-hidden"
>
<div className="w-full aspect-square md:aspect-16/6 mask-fade-top-long">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Footer video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
</div>
<div className="w-full py-8 primary-button text-primary-cta-text">
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
<h2 className="text-4xl font-medium">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<a
key={item.label}
href={item.href}
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
>
{item.label}
</a>
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<a
key={link.label}
href={link.href}
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</div>
</footer>
);
};
export default FooterSimpleMedia;

View File

@@ -0,0 +1,70 @@
type FooterColumn = {
title: string;
items: { label: string; href?: string; onClick?: () => void }[];
};
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
const FooterSimpleReveal = ({
brand,
columns,
copyright,
links,
}: {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
}) => {
return (
<footer
data-webild-section="FooterSimpleReveal"
aria-label="Site footer"
className="w-full py-8 mt-20 primary-button text-primary-cta-text"
>
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row gap-8 md:gap-0 justify-between items-start mb-8">
<h2 className="text-4xl font-medium">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-8 md:gap-8">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<a
key={item.label}
href={item.href}
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity"
>
{item.label}
</a>
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<a
key={link.label}
href={link.href}
className="text-sm opacity-50 hover:opacity-75 transition-opacity"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</footer>
);
};
export default FooterSimpleReveal;

View File

@@ -0,0 +1,140 @@
import { motion } from "motion/react";
type HeroBillboardProps = {
tag?: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
avatars?: { src: string }[];
avatarsLabel?: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboard = ({
tag,
title,
description,
primaryButton,
secondaryButton,
avatars,
avatarsLabel,
imageSrc,
videoSrc,
}: HeroBillboardProps) => {
return (
<section
data-webild-section="HeroBillboard"
aria-label="Hero section"
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
>
<div className="flex flex-col gap-8 w-content-width mx-auto">
<div className="flex flex-col items-center gap-3 text-center">
{avatars && avatars.length > 0 ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex items-center gap-3"
>
<div className="flex -space-x-2">
{avatars.map((avatar, i) => (
<img
key={i}
src={avatar.src}
alt=""
className="w-8 h-8 rounded-full border-2 border-background object-cover"
/>
))}
</div>
{avatarsLabel ? (
<span className="text-sm">{avatarsLabel}</span>
) : null}
</motion.div>
) : tag ? (
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
) : null}
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
</div>
</section>
);
};
export default HeroBillboard;

View File

@@ -0,0 +1,100 @@
import { motion } from "motion/react";
type HeroBillboardBrandProps = {
brand: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboardBrand = ({
brand,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroBillboardBrandProps) => {
return (
<section
data-webild-section="HeroBillboardBrand"
aria-label="Hero section"
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
>
<div className="flex flex-col gap-8 w-content-width mx-auto">
<div className="flex flex-col items-end gap-5">
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-full text-9xl font-semibold text-balance text-right leading-none"
>
{brand}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="w-full md:w-1/2 text-lg md:text-2xl leading-tight text-balance text-right"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-end gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
</div>
</section>
);
};
export default HeroBillboardBrand;

View File

@@ -0,0 +1,120 @@
import { motion } from "motion/react";
import Marquee from "@/components/ui/marquee";
type HeroBillboardCarouselProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroBillboardCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroBillboardCarouselProps) => {
return (
<section
data-webild-section="HeroBillboardCarousel"
aria-label="Hero section"
className="relative flex flex-col items-center justify-center gap-8 w-full min-h-svh py-25"
>
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="w-full mask-fade-x"
>
<Marquee speed={60} pauseOnHover={false}>
{items.map((item, i) => (
<div
key={i}
className="shrink-0 w-60 md:w-80 aspect-4/5 card rounded-theme overflow-hidden p-2"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</Marquee>
</motion.div>
</section>
);
};
export default HeroBillboardCarousel;

View File

@@ -0,0 +1,112 @@
import { motion } from "motion/react";
type HeroBillboardScrollProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboardScroll = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroBillboardScrollProps) => {
return (
<section
data-webild-section="HeroBillboardScroll"
aria-label="Hero section"
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
>
<div className="w-content-width mx-auto">
<div className="flex flex-col items-center gap-3 text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 40, rotateX: 15 }}
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-4/5 md:aspect-video mt-8"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
</div>
</section>
);
};
export default HeroBillboardScroll;

View File

@@ -0,0 +1,163 @@
import { motion } from "motion/react";
import { Star } from "lucide-react";
type Testimonial = {
name: string;
handle: string;
text: string;
rating: number;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
type HeroBillboardTestimonialProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
testimonials: Testimonial[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboardTestimonial = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
testimonials,
}: HeroBillboardTestimonialProps) => {
const testimonial = testimonials[0];
return (
<section
data-webild-section="HeroBillboardTestimonial"
aria-label="Hero section"
className="relative w-full pt-25 pb-20 md:py-hero-page-padding"
>
<div className="flex flex-col gap-8 w-content-width mx-auto">
<div className="flex flex-col items-center gap-3 text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="relative card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full aspect-3/4 md:aspect-video"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
<figure className="absolute bottom-6 left-6 right-6 md:right-auto md:bottom-8 md:left-8 md:max-w-sm card rounded-theme p-4 flex flex-col gap-3">
<div className="flex gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
strokeWidth={1.5}
/>
))}
</div>
<blockquote className="text-base leading-tight text-balance">
{testimonial.text}
</blockquote>
<figcaption className="flex items-center gap-3">
{testimonial.videoSrc ? (
<video
src={testimonial.videoSrc}
autoPlay
muted
loop
playsInline
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<img
src={testimonial.imageSrc}
alt=""
className="w-10 h-10 rounded-full object-cover"
/>
)}
<div className="flex flex-col">
<span className="text-sm font-medium">{testimonial.name}</span>
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
</div>
</figcaption>
</figure>
</motion.div>
</div>
</section>
);
};
export default HeroBillboardTestimonial;

View File

@@ -0,0 +1,126 @@
import { motion } from "motion/react";
type HeroBillboardTiltedCarouselProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroBillboardTiltedCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroBillboardTiltedCarouselProps) => {
const tiltClasses = [
"-rotate-6 -translate-y-3",
"rotate-3 translate-y-3",
"-rotate-3 -translate-y-2",
"rotate-6 translate-y-4",
"-rotate-6 -translate-y-3",
"rotate-3 translate-y-3",
];
return (
<section
data-webild-section="HeroBillboardTiltedCarousel"
aria-label="Hero section"
className="relative flex flex-col items-center justify-center gap-8 w-full min-h-svh py-25 overflow-hidden"
>
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="flex justify-center items-center gap-3 w-full max-w-[120vw] -mx-8"
>
{items.map((item, i) => (
<div
key={i}
className={`shrink-0 w-50 md:w-60 aspect-4/5 card rounded-theme overflow-hidden p-2 transform ${tiltClasses[i % tiltClasses.length]}`}
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</motion.div>
</section>
);
};
export default HeroBillboardTiltedCarousel;

View File

@@ -0,0 +1,101 @@
import { motion } from "motion/react";
type HeroBrandProps = {
brand: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBrand = ({
brand,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroBrandProps) => {
return (
<section
data-webild-section="HeroBrand"
aria-label="Hero section"
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 z-1 h-3/5 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
/>
<div className="relative z-10 w-content-width mx-auto pb-5">
<div className="flex flex-col gap-5">
<div className="w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-5">
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-full md:w-1/2 text-lg md:text-2xl text-balance text-primary-cta-text leading-tight"
>
{description}
</motion.p>
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
<div className="flex flex-wrap gap-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="font-semibold text-primary-cta-text text-9xl leading-none text-balance"
>
{brand}
</motion.h1>
</div>
</div>
</section>
);
};
export default HeroBrand;

View File

@@ -0,0 +1,117 @@
import { motion } from "motion/react";
type HeroBrandCarouselProps = {
brand: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroBrandCarousel = ({
brand,
description,
primaryButton,
secondaryButton,
items,
}: HeroBrandCarouselProps) => {
const featured = items[0];
return (
<section
data-webild-section="HeroBrandCarousel"
aria-label="Hero section"
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
>
{featured.videoSrc ? (
<video
src={featured.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={featured.imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 z-1 h-3/5 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
/>
<div className="relative z-10 w-content-width mx-auto pb-5">
<div className="flex flex-col gap-5">
<div className="w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-5">
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-full md:w-1/2 text-lg md:text-2xl text-balance text-primary-cta-text leading-tight"
>
{description}
</motion.p>
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
<div className="flex flex-wrap gap-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="font-semibold text-primary-cta-text text-9xl leading-none text-balance"
>
{brand}
</motion.h1>
<div className="flex gap-3 pb-5">
{items.map((_, i) => (
<div
key={i}
className="relative h-1 w-full rounded-full overflow-hidden bg-primary-cta-text/20"
aria-hidden="true"
>
<div
className={`absolute inset-0 bg-primary-cta-text rounded-full origin-left ${i === 0 ? "scale-x-100" : "scale-x-0"}`}
/>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default HeroBrandCarousel;

View File

@@ -0,0 +1,144 @@
import { motion } from "motion/react";
import Marquee from "@/components/ui/marquee";
type HeroCenteredLogosProps = {
avatars: { src: string }[];
avatarText: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
logos: string[];
hideMedia?: boolean;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroCenteredLogos = ({
avatars,
avatarText,
title,
description,
primaryButton,
secondaryButton,
logos,
imageSrc,
videoSrc,
hideMedia = false,
}: HeroCenteredLogosProps) => {
return (
<section
data-webild-section="HeroCenteredLogos"
aria-label="Hero section"
className="relative h-svh w-full flex flex-col"
>
{!hideMedia && (
<div className="absolute inset-0 z-0">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover"
/>
)}
<div className="absolute inset-0 bg-background/80" />
</div>
)}
<div className="relative z-10 flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-3 pt-8 w-content-width mx-auto text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex items-center gap-3"
>
<div className="flex -space-x-2">
{avatars.map((avatar, i) => (
<img
key={i}
src={avatar.src}
alt=""
className="w-10 h-10 rounded-full border-2 border-background object-cover"
/>
))}
</div>
<span className="text-sm">{avatarText}</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="md:max-w-8/10 text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-2">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="relative z-10 w-content-width mx-auto pb-8 mask-fade-x"
>
<Marquee speed={30} pauseOnHover={false}>
{logos.map((logo, i) => (
<div key={i} className="shrink-0 px-4 py-2 card rounded-theme">
<span className="text-xl font-semibold whitespace-nowrap opacity-75">
{logo}
</span>
</div>
))}
</Marquee>
</motion.div>
</section>
);
};
export default HeroCenteredLogos;

View File

@@ -0,0 +1,137 @@
import { motion } from "motion/react";
type HeroOverlayProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
avatars?: { src: string }[];
avatarsLabel?: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroOverlay = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
avatars,
avatarsLabel,
}: HeroOverlayProps) => {
return (
<section
data-webild-section="HeroOverlay"
aria-label="Hero section"
className="relative w-full h-svh overflow-hidden flex flex-col justify-end"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 z-1 h-3/4 bg-gradient-to-t from-foreground/70 via-foreground/30 to-transparent"
/>
<div className="relative z-10 w-content-width mx-auto pb-8 md:pb-25">
<div className="flex flex-col gap-3 w-full md:w-3/5 lg:w-1/2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-fit card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-primary-cta-text text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-lg md:text-xl text-primary-cta-text leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
{avatars && avatars.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.45, ease: "easeOut" }}
className="mt-4 flex items-center gap-3"
>
<div className="flex -space-x-2">
{avatars.map((avatar, i) => (
<img
key={i}
src={avatar.src}
alt=""
className="w-10 h-10 rounded-full border-2 border-background object-cover"
/>
))}
</div>
{avatarsLabel ? (
<span className="text-sm text-primary-cta-text">{avatarsLabel}</span>
) : null}
</motion.div>
)}
</div>
</div>
</section>
);
};
export default HeroOverlay;

View File

@@ -0,0 +1,166 @@
import { motion } from "motion/react";
import { Star } from "lucide-react";
type Testimonial = {
name: string;
handle: string;
text: string;
rating: number;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
type HeroOverlayTestimonialProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
testimonials: Testimonial[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroOverlayTestimonial = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
testimonials,
}: HeroOverlayTestimonialProps) => {
const testimonial = testimonials[0];
return (
<section
data-webild-section="HeroOverlayTestimonial"
aria-label="Hero section"
className="relative w-full h-svh overflow-hidden flex flex-col justify-start"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<img
src={imageSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 z-1 h-3/4 bg-gradient-to-b from-foreground/60 via-foreground/30 to-transparent"
/>
<div className="relative z-10 w-content-width mx-auto pt-35">
<div className="flex flex-col gap-3 w-full md:w-3/5 lg:w-1/2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="w-fit card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-primary-cta-text text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-lg md:text-xl text-primary-cta-text leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.figure
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.45, ease: "easeOut" }}
className="absolute z-10 bottom-3 left-3 right-3 md:left-auto md:bottom-8 md:right-8 md:max-w-sm card rounded-theme p-4 flex flex-col gap-3"
>
<div className="flex gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
strokeWidth={1.5}
/>
))}
</div>
<blockquote className="text-base leading-tight text-balance">
{testimonial.text}
</blockquote>
<figcaption className="flex items-center gap-3">
{testimonial.videoSrc ? (
<video
src={testimonial.videoSrc}
autoPlay
muted
loop
playsInline
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<img
src={testimonial.imageSrc}
alt=""
className="w-10 h-10 rounded-full object-cover"
/>
)}
<div className="flex flex-col">
<span className="text-sm font-medium">{testimonial.name}</span>
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
</div>
</figcaption>
</motion.figure>
</section>
);
};
export default HeroOverlayTestimonial;

View File

@@ -0,0 +1,112 @@
import { motion } from "motion/react";
type HeroSplitProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroSplitProps) => {
return (
<section
data-webild-section="HeroSplit"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col md:flex-row items-center gap-10 md:gap-20 w-content-width mx-auto">
<div className="flex flex-col items-center md:items-start gap-3 w-full md:w-1/2">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
>
{description}
</motion.p>
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 w-full md:w-1/2 h-100 md:h-[65vh] md:max-h-[75svh]"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
</div>
</section>
);
};
export default HeroSplit;

View File

@@ -0,0 +1,143 @@
import { motion } from "motion/react";
type KpiItem = {
value: string;
label: string;
};
type HeroSplitKpiProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
kpis: [KpiItem, KpiItem, KpiItem];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroSplitKpi = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
kpis,
}: HeroSplitKpiProps) => {
const kpiPositions = [
"top-[5%] left-0",
"top-[40%] right-0",
"bottom-[5%] left-[5%]",
];
return (
<section
data-webild-section="HeroSplitKpi"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
<div className="w-full md:w-1/2">
<div className="flex flex-col items-center md:items-start gap-3">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
>
{description}
</motion.p>
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<div className="relative w-full md:w-1/2 h-100 md:h-[65vh] md:max-h-[75svh]">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="w-full h-full card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5 scale-80"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</motion.div>
{kpis.map((kpi, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.5, delay: 0.4 + i * 0.1, ease: "easeOut" }}
className={`absolute flex flex-col items-center card rounded-theme backdrop-blur-sm p-4 ${kpiPositions[i]}`}
>
<p className="text-2xl md:text-4xl font-medium">{kpi.value}</p>
<p className="text-sm md:text-base text-muted-foreground">{kpi.label}</p>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default HeroSplitKpi;

View File

@@ -0,0 +1,124 @@
import { motion } from "motion/react";
type HeroSplitMediaGridProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: [
{ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never },
{ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }
];
};
const HeroSplitMediaGrid = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroSplitMediaGridProps) => {
return (
<section
data-webild-section="HeroSplitMediaGrid"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
<div className="w-full md:w-1/2">
<div className="flex flex-col items-center md:items-start gap-3">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
>
{description}
</motion.p>
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="w-full md:w-1/2 grid grid-cols-2 gap-3"
>
{items.map((item, i) => (
<div
key={i}
className="h-80 md:h-[55vh] card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</motion.div>
</div>
</section>
);
};
export default HeroSplitMediaGrid;

View File

@@ -0,0 +1,165 @@
import { motion } from "motion/react";
import { Star } from "lucide-react";
type Testimonial = {
name: string;
handle: string;
text: string;
rating: number;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
type HeroSplitTestimonialProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
testimonials: Testimonial[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroSplitTestimonial = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
testimonials,
}: HeroSplitTestimonialProps) => {
const testimonial = testimonials[0];
return (
<section
data-webild-section="HeroSplitTestimonial"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
<div className="w-full md:w-1/2">
<div className="flex flex-col items-center md:items-start gap-3">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
>
{description}
</motion.p>
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="relative w-full md:w-1/2 aspect-3/4 md:aspect-auto md:h-[65vh] md:max-h-[75svh] card rounded-theme overflow-hidden p-3 xl:p-4 2xl:p-5"
>
{videoSrc ? (
<video
src={videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero video"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
<figure className="absolute bottom-6 left-6 right-6 md:left-auto md:max-w-1/2 card rounded-theme p-4 flex flex-col gap-3">
<div className="flex gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-5 h-5 text-accent ${i < testimonial.rating ? "fill-accent" : "fill-transparent"}`}
strokeWidth={1.5}
/>
))}
</div>
<blockquote className="text-base leading-tight text-balance">
{testimonial.text}
</blockquote>
<figcaption className="flex items-center gap-3">
{testimonial.videoSrc ? (
<video
src={testimonial.videoSrc}
autoPlay
muted
loop
playsInline
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<img
src={testimonial.imageSrc}
alt=""
className="w-10 h-10 rounded-full object-cover"
/>
)}
<div className="flex flex-col">
<span className="text-sm font-medium">{testimonial.name}</span>
<span className="text-sm text-muted-foreground">{testimonial.handle}</span>
</div>
</figcaption>
</figure>
</motion.div>
</div>
</section>
);
};
export default HeroSplitTestimonial;

View File

@@ -0,0 +1,156 @@
import { motion } from "motion/react";
type HeroSplitVerticalMarqueeProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
leftItems: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
rightItems: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroSplitVerticalMarquee = ({
tag,
title,
description,
primaryButton,
secondaryButton,
leftItems,
rightItems,
}: HeroSplitVerticalMarqueeProps) => {
return (
<section
data-webild-section="HeroSplitVerticalMarquee"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col md:flex-row items-center gap-8 w-content-width mx-auto">
<div className="w-full md:w-1/2">
<div className="flex flex-col items-center md:items-start gap-3">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
>
{description}
</motion.p>
<div className="flex flex-wrap max-md:justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="w-full md:w-1/2 h-100 md:h-[75vh] flex gap-3 overflow-hidden"
>
<div className="flex-1 overflow-hidden mask-fade-y">
<div className="flex flex-col gap-3 animate-marquee-vertical">
{[...leftItems, ...leftItems, ...leftItems, ...leftItems].map((item, i) => (
<div
key={i}
className="shrink-0 aspect-square card rounded-theme overflow-hidden p-2 xl:p-3 2xl:p-4"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</div>
</div>
<div className="flex-1 overflow-hidden mask-fade-y">
<div className="flex flex-col gap-3 animate-marquee-vertical-reverse">
{[...rightItems, ...rightItems, ...rightItems, ...rightItems].map((item, i) => (
<div
key={i}
className="shrink-0 aspect-square card rounded-theme overflow-hidden p-2 xl:p-3 2xl:p-4"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</div>
</div>
</motion.div>
</div>
</section>
);
};
export default HeroSplitVerticalMarquee;

View File

@@ -0,0 +1,162 @@
import { motion } from "motion/react";
type HeroTiltedCardsProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroTiltedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroTiltedCardsProps) => {
const galleryStyles = [
"-rotate-6 z-10 -translate-y-5",
"rotate-6 z-20 translate-y-5 -ml-15",
"-rotate-6 z-30 -translate-y-5 -ml-15",
"rotate-6 z-40 translate-y-5 -ml-15",
"-rotate-6 z-50 -translate-y-5 -ml-15",
];
return (
<section
data-webild-section="HeroTiltedCards"
aria-label="Hero section"
className="relative flex items-center w-full h-fit md:h-svh pt-25 pb-20 md:py-0"
>
<div className="flex flex-col items-center gap-8 w-full md:w-content-width mx-auto">
<div className="flex flex-col items-center gap-3 w-content-width mx-auto text-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-balance"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="text-base md:text-lg leading-tight text-balance"
>
{description}
</motion.p>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<motion.a
href={primaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.25, ease: "easeOut" }}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</motion.a>
<motion.a
href={secondaryButton.href}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.35, ease: "easeOut" }}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</motion.a>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="hidden md:flex justify-center items-center w-full"
>
<div className="flex items-center justify-center">
{items.map((item, i) => (
<div
key={i}
className={`relative w-1/4 aspect-4/5 card rounded-theme overflow-hidden p-2 transition-transform duration-500 ease-out hover:scale-110 ${galleryStyles[i % galleryStyles.length]}`}
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="md:hidden grid grid-cols-2 gap-3 w-content-width mx-auto"
>
{items.slice(0, 4).map((item, i) => (
<div
key={i}
className="aspect-4/5 card rounded-theme overflow-hidden p-2"
>
{item.videoSrc ? (
<video
src={item.videoSrc}
autoPlay
muted
loop
playsInline
aria-label="Hero media"
className="w-full h-full object-cover rounded-theme"
/>
) : (
<img
src={item.imageSrc}
alt=""
className="w-full h-full object-cover rounded-theme"
/>
)}
</div>
))}
</motion.div>
</div>
</section>
);
};
export default HeroTiltedCards;

View File

@@ -0,0 +1,100 @@
import { motion } from "motion/react";
type ContentItem =
| { type: "paragraph"; text: string }
| { type: "list"; items: string[] }
| { type: "numbered-list"; items: string[] };
type ContentSection = {
heading: string;
content: ContentItem[];
};
const PolicyContent = ({
title,
subtitle,
sections,
}: {
title: string;
subtitle?: string;
sections: ContentSection[];
}) => {
return (
<section
data-webild-section="PolicyContent"
aria-label="Policy content"
className="relative w-full pt-40 pb-20"
>
<div className="w-content-width mx-auto">
<div className="md:max-w-1/2 mx-auto flex flex-col gap-5">
<div className="flex flex-col gap-3">
<motion.h1
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="text-3xl md:text-4xl font-medium leading-tight text-balance"
>
{title}
</motion.h1>
{subtitle && (
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
className="text-sm opacity-50"
>
{subtitle}
</motion.p>
)}
</div>
<div className="w-full h-px bg-foreground/20" />
<div className="flex flex-col gap-5">
{sections.map((policySection, sectionIndex) => (
<motion.div
key={policySection.heading}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.05 + sectionIndex * 0.05, ease: "easeOut" }}
className="flex flex-col gap-3"
>
<h2 className="text-xl md:text-2xl font-medium leading-tight">{policySection.heading}</h2>
{policySection.content.map((item, i) => {
if (item.type === "paragraph") {
return (
<p key={i} className="text-sm md:text-base opacity-75 leading-relaxed">
{item.text}
</p>
);
}
if (item.type === "numbered-list") {
return (
<ol key={i} className="flex flex-col gap-3 pl-5 text-sm md:text-base opacity-75 leading-relaxed list-decimal">
{item.items.map((li, j) => (
<li key={j}>{li}</li>
))}
</ol>
);
}
return (
<ul key={i} className="flex flex-col gap-3 pl-5 text-sm md:text-base opacity-75 leading-relaxed list-disc">
{item.items.map((li, j) => (
<li key={j}>{li}</li>
))}
</ul>
);
})}
</motion.div>
))}
</div>
</div>
</div>
</section>
);
};
export default PolicyContent;

View File

@@ -0,0 +1,122 @@
import { motion } from "motion/react";
import { Check } from "lucide-react";
type Metric = {
value: string;
title: string;
features: string[];
};
type MetricsFeatureCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
metrics: Metric[];
};
const MetricsFeatureCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
metrics,
}: MetricsFeatureCardsProps) => {
return (
<section
data-webild-section="MetricsFeatureCards"
aria-label="Metrics section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
>
{metrics.map((metric) => (
<div
key={metric.value}
className="flex flex-col justify-between gap-5 p-5 h-full card rounded-theme"
>
<div className="flex flex-col gap-0">
<span className="text-7xl font-medium leading-none truncate">{metric.value}</span>
<span className="text-xl truncate">{metric.title}</span>
</div>
<div className="flex flex-col gap-3 pt-5 border-t border-accent">
{metric.features.map((feature) => (
<div key={feature} className="flex items-start gap-3">
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded-full">
<Check className="w-4 h-4 text-primary-cta-text" />
</div>
<span className="text-sm leading-tight">{feature}</span>
</div>
))}
</div>
</div>
))}
</motion.div>
</div>
</section>
);
};
export default MetricsFeatureCards;

View File

@@ -0,0 +1,129 @@
import { motion } from "motion/react";
import { Sparkles, type LucideIcon } from "lucide-react";
type Metric = {
value: string;
title: string;
description: string;
icon: string | LucideIcon;
};
type MetricsGradientCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
metrics: Metric[];
};
const MetricsGradientCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
metrics,
}: MetricsGradientCardsProps) => {
return (
<section
data-webild-section="MetricsGradientCards"
aria-label="Metrics section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
>
{metrics.map((metric) => (
<div
key={metric.value}
className="relative flex flex-col items-center justify-center gap-0 p-5 min-h-70 h-full card rounded-theme"
>
<span
className="text-9xl font-medium leading-none text-center truncate"
style={{
backgroundImage:
"linear-gradient(to bottom, var(--color-foreground) 0%, var(--color-foreground) 20%, transparent 72%)",
WebkitBackgroundClip: "text",
backgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
>
{metric.value}
</span>
<span className="mt-[-0.75em] text-4xl font-medium text-center truncate">
{metric.title}
</span>
<p className="max-w-9/10 md:max-w-7/10 mt-2 text-base leading-tight text-center line-clamp-2">
{metric.description}
</p>
<div className="absolute bottom-5 left-5 flex items-center justify-center size-10 primary-button rounded-full">
<Sparkles className="w-4 h-4 text-primary-cta-text" />
</div>
</div>
))}
</motion.div>
</div>
</section>
);
};
export default MetricsGradientCards;

View File

@@ -0,0 +1,114 @@
import { motion } from "motion/react";
import { Sparkles, type LucideIcon } from "lucide-react";
type Metric = {
icon: string | LucideIcon;
title: string;
value: string;
};
type MetricsIconCardsProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
metrics: Metric[];
};
const MetricsIconCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
metrics,
}: MetricsIconCardsProps) => {
return (
<section
data-webild-section="MetricsIconCards"
aria-label="Metrics section"
className="relative w-full py-20"
>
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto">
<motion.span
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="card rounded-full px-3 py-1 mb-1 text-sm"
>
{tag}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.05, ease: "easeOut" }}
className="text-6xl font-medium text-center text-balance"
>
{title}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
className="md:max-w-6/10 text-lg leading-tight text-center"
>
{description}
</motion.p>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && (
<a
href={primaryButton.href}
className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text"
>
{primaryButton.text}
</a>
)}
{secondaryButton && (
<a
href={secondaryButton.href}
className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text"
>
{secondaryButton.text}
</a>
)}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-3 gap-5 w-content-width mx-auto"
>
{metrics.map((metric) => (
<div
key={metric.value}
className="flex flex-col items-center justify-center gap-3 p-5 min-h-70 h-full card rounded-theme"
>
<div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center size-8 primary-button rounded-full">
<Sparkles className="w-4 h-4 text-primary-cta-text" />
</div>
<span className="text-xl truncate">{metric.title}</span>
</div>
<span className="text-7xl font-medium leading-none truncate">{metric.value}</span>
</div>
))}
</motion.div>
</div>
</section>
);
};
export default MetricsIconCards;

Some files were not shown because too many files have changed in this diff Show More