Initial commit
This commit is contained in:
83
.claude/rules/accessibility.md
Normal file
83
.claude/rules/accessibility.md
Normal 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
|
||||
95
.claude/rules/animations.md
Normal file
95
.claude/rules/animations.md
Normal 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
159
.claude/rules/components.md
Normal 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>
|
||||
```
|
||||
123
.claude/rules/registry.md
Normal file
123
.claude/rules/registry.md
Normal 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
188
.claude/rules/styling.md
Normal 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)]" />
|
||||
```
|
||||
69
.claude/rules/transformation.md
Normal file
69
.claude/rules/transformation.md
Normal 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
|
||||
41
.claude/skills/add-animation/SKILL.md
Normal file
41
.claude/skills/add-animation/SKILL.md
Normal 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.
|
||||
47
.claude/skills/add-to-registry/SKILL.md
Normal file
47
.claude/skills/add-to-registry/SKILL.md
Normal 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.
|
||||
61
.claude/skills/code-review/SKILL.md
Normal file
61
.claude/skills/code-review/SKILL.md
Normal 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
|
||||
47
.claude/skills/debug/SKILL.md
Normal file
47
.claude/skills/debug/SKILL.md
Normal 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.
|
||||
57
.claude/skills/performance/SKILL.md
Normal file
57
.claude/skills/performance/SKILL.md
Normal 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.
|
||||
55
.claude/skills/refactor/SKILL.md
Normal file
55
.claude/skills/refactor/SKILL.md
Normal 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.
|
||||
59
.claude/skills/scaffold-component/SKILL.md
Normal file
59
.claude/skills/scaffold-component/SKILL.md
Normal 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
4
.env
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
VITE_API_URL=http://localhost:3001
|
||||
VITE_PROJECT_ID=cf46d827-af05-4e4a-b0ae-c1cd26eeea45
|
||||
|
||||
46
.gitea/workflows/build.yml
Normal file
46
.gitea/workflows/build.yml
Normal 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
24
.gitignore
vendored
Normal 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
73
README.md
Normal 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
218
STRUCTURE.md
Normal 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.
|
||||
|
||||
**No dynamic classes in sections.** Avoid `cls(condition && "class")`. Keep it explicit.
|
||||
|
||||
**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 |
|
||||
|
||||
---
|
||||
58
THEME_PROVIDER_OPTIONS.txt
Normal file
58
THEME_PROVIDER_OPTIONS.txt
Normal 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"
|
||||
|
||||
================================================================================
|
||||
677
colorThemes.json
Normal file
677
colorThemes.json
Normal file
@@ -0,0 +1,677 @@
|
||||
{
|
||||
"lightTheme": {
|
||||
"minimalDarkBlue": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000612e6",
|
||||
"--primary-cta": "#15479c",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000612e6"
|
||||
},
|
||||
"minimalDarkGreen": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000f06e6",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000f06e6"
|
||||
},
|
||||
"minimalLightRed": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120006e6",
|
||||
"--primary-cta": "#e63946",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120006e6"
|
||||
},
|
||||
"minimalBrightBlue": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000612e6",
|
||||
"--primary-cta": "#106EFB",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#106EFB",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000612e6"
|
||||
},
|
||||
"minimalBrightOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#E34400",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#E34400",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"minimalGoldenOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#FF7B05",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#FF7B05",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#ff8c42",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"darkBlue": {
|
||||
"--background": "#f5faff",
|
||||
"--card": "#f1f8ff",
|
||||
"--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": "#f7fffa",
|
||||
"--foreground": "#001a0a",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#a8d9be",
|
||||
"--background-accent": "#6bbf8e",
|
||||
"--primary-cta-text": "#fafffb",
|
||||
"--secondary-cta-text": "#001a0a"
|
||||
},
|
||||
"lightRed": {
|
||||
"--background": "#fffafa",
|
||||
"--card": "#fff7f7",
|
||||
"--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": "#f7f5ff",
|
||||
"--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"
|
||||
},
|
||||
"warmgrayIndigo": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#0c1325",
|
||||
"--primary-cta": "#0b07ff",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#93b7ff",
|
||||
"--background-accent": "#a8bae8",
|
||||
"--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": "#fffefe",
|
||||
"--card": "#f6f7f4",
|
||||
"--foreground": "#080908",
|
||||
"--primary-cta": "#0e3a29",
|
||||
"--secondary-cta": "#e7eecd",
|
||||
"--accent": "#35c18b",
|
||||
"--background-accent": "#ecebe4",
|
||||
"--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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"lightBlueWhite": {
|
||||
"--background": "#010912",
|
||||
"--card": "#152840",
|
||||
"--foreground": "#e6f0ff",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#0e1a29",
|
||||
"--accent": "#3f5c79",
|
||||
"--background-accent": "#004a93",
|
||||
"--primary-cta-text": "#010912",
|
||||
"--secondary-cta-text": "#ffffff"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"crimson": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#f5f5f5",
|
||||
"--primary-cta": "#ff0000",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#991b1b",
|
||||
"--background-accent": "#7f1d1d",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#ffffff"
|
||||
},
|
||||
"midnightIce": {
|
||||
"--background": "#000000",
|
||||
"--card": "#0c0c0c",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#000000",
|
||||
"--accent": "#535353",
|
||||
"--background-accent": "#CEE7FF",
|
||||
"--primary-cta-text": "#000000",
|
||||
"--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"
|
||||
},
|
||||
"orangeBlueAccent": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#e34400",
|
||||
"--secondary-cta": "#010101",
|
||||
"--accent": "#ff7b05",
|
||||
"--background-accent": "#106efb",
|
||||
"--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"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffaf5e6",
|
||||
"--primary-cta": "#ffaa70",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373",
|
||||
"--primary-cta-text": "#0a0a0a",
|
||||
"--secondary-cta-text": "#fffaf5e6"
|
||||
},
|
||||
"minimalLightYellow": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffffae6",
|
||||
"--primary-cta": "#fde047",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373",
|
||||
"--primary-cta-text": "#0a0a0a",
|
||||
"--secondary-cta-text": "#fffffae6"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"indigo": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1f1f40",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d0d2b",
|
||||
"--accent": "#3d2880",
|
||||
"--background-accent": "#663cff",
|
||||
"--primary-cta-text": "#0a051a",
|
||||
"--secondary-cta-text": "#d4d4f6"
|
||||
},
|
||||
"forest": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1a2f1d",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d200f",
|
||||
"--accent": "#1a3d1f",
|
||||
"--background-accent": "#355e3b",
|
||||
"--primary-cta-text": "#0a1a0c",
|
||||
"--secondary-cta-text": "#d4f6d8"
|
||||
},
|
||||
"mint": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1a2a1a",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d1a0d",
|
||||
"--accent": "#2d4a2d",
|
||||
"--background-accent": "#c1e1c1",
|
||||
"--primary-cta-text": "#0a150a",
|
||||
"--secondary-cta-text": "#e1f6e1"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
cssOptions.json
Normal file
41
cssOptions.json
Normal 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);"
|
||||
}
|
||||
}
|
||||
26
eslint.config.js
Normal file
26
eslint.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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',
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
237
fontThemes.json
Normal file
237
fontThemes.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
index.html
Normal file
19
index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Laude Coffee | Artisan Roasted Specialty Coffee</title>
|
||||
<meta property="og:title" content="Laude Coffee | Artisan Roasted Specialty Coffee" />
|
||||
<meta name="twitter:title" content="Laude Coffee | Artisan Roasted Specialty Coffee" />
|
||||
<meta name="description" content="Experience artisan-roasted specialty coffee at Laude. Sourced responsibly, crafted with precision, and served fresh daily." />
|
||||
<meta property="og:description" content="Experience artisan-roasted specialty coffee at Laude. Sourced responsibly, crafted with precision, and served fresh daily." />
|
||||
<meta name="twitter:description" content="Experience artisan-roasted specialty coffee at Laude. Sourced responsibly, crafted with precision, and served fresh daily." />
|
||||
<meta name="keywords" content="coffee shop, specialty coffee, artisanal roasting, cafe, fresh coffee, Laude Coffee" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"motion": "^12.38.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.2.4",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
2228
pnpm-lock.yaml
generated
Normal file
2228
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
1010
registry.json
Normal file
1010
registry.json
Normal file
File diff suppressed because it is too large
Load Diff
277
src/App.tsx
Normal file
277
src/App.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import AboutTestimonial from '@/components/sections/about/AboutTestimonial';
|
||||
import ContactSplitEmail from '@/components/sections/contact/ContactSplitEmail';
|
||||
import FaqSplitMedia from '@/components/sections/faq/FaqSplitMedia';
|
||||
import FeaturesRevealCards from '@/components/sections/features/FeaturesRevealCards';
|
||||
import FooterBrandReveal from '@/components/sections/footer/FooterBrandReveal';
|
||||
import HeroBillboard from '@/components/sections/hero/HeroBillboard';
|
||||
import MetricsMinimalCards from '@/components/sections/metrics/MetricsMinimalCards';
|
||||
import NavbarCentered from '@/components/ui/NavbarCentered';
|
||||
import SocialProofMarquee from '@/components/sections/social-proof/SocialProofMarquee';
|
||||
import TestimonialRatingCards from '@/components/sections/testimonial/TestimonialRatingCards';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarCentered
|
||||
logo="Laude"
|
||||
navItems={[
|
||||
{
|
||||
name: "About",
|
||||
href: "#about",
|
||||
},
|
||||
{
|
||||
name: "Experience",
|
||||
href: "#features",
|
||||
},
|
||||
{
|
||||
name: "Reviews",
|
||||
href: "#testimonials",
|
||||
},
|
||||
{
|
||||
name: "Contact",
|
||||
href: "#contact",
|
||||
},
|
||||
]}
|
||||
ctaButton={{
|
||||
text: "Visit Us",
|
||||
href: "#contact",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroBillboard
|
||||
tag="Since 2012"
|
||||
title="Experience Perfection in Every Cup"
|
||||
description="Laude brings the finest artisanal beans from remote mountains to your morning ritual. Discover the deep, rich flavors crafted by master roasters."
|
||||
primaryButton={{
|
||||
text: "Explore Menu",
|
||||
href: "#features",
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: "Visit Us",
|
||||
href: "#contact",
|
||||
}}
|
||||
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=dovsmb"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="about" data-section="about">
|
||||
<AboutTestimonial
|
||||
tag="Our Philosophy"
|
||||
quote="Coffee is not just a drink, it's a bridge between human beings and the earth's most subtle flavors."
|
||||
author="Elena Rossi"
|
||||
role="Founder & Lead Roaster"
|
||||
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=aaqeli"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="features" data-section="features">
|
||||
<FeaturesRevealCards
|
||||
tag="Artisan Craft"
|
||||
title="Why Laude Stands Out"
|
||||
description="We treat every bean with meticulous care, from selection to final pour."
|
||||
items={[
|
||||
{
|
||||
title: "Ethically Sourced",
|
||||
description: "Working directly with small-lot farmers to ensure fair trade and exceptional quality.",
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=dfegx8",
|
||||
},
|
||||
{
|
||||
title: "Precision Roasting",
|
||||
description: "Small-batch roasting techniques that unlock complex flavor profiles in every roast.",
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=0surv3",
|
||||
},
|
||||
{
|
||||
title: "Handmade Treats",
|
||||
description: "Freshly baked pastries every morning, perfectly paired with our signature coffee blends.",
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=o1g220",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="metrics" data-section="metrics">
|
||||
<MetricsMinimalCards
|
||||
tag="Our Impact"
|
||||
title="Laude in Numbers"
|
||||
metrics={[
|
||||
{
|
||||
value: "12+",
|
||||
description: "Years of Roasting",
|
||||
},
|
||||
{
|
||||
value: "450k",
|
||||
description: "Cups Served",
|
||||
},
|
||||
{
|
||||
value: "28",
|
||||
description: "Sourcing Partners",
|
||||
},
|
||||
{
|
||||
value: "99%",
|
||||
description: "Customer Satisfaction",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="testimonials" data-section="testimonials">
|
||||
<TestimonialRatingCards
|
||||
tag="Love from Locals"
|
||||
title="What Our Community Says"
|
||||
description="Heartfelt feedback from those who start their day with a Laude blend."
|
||||
testimonials={[
|
||||
{
|
||||
name: "James D.",
|
||||
role: "Graphic Designer",
|
||||
quote: "The best espresso in the city, no contest. The atmosphere is just perfect.",
|
||||
rating: 5,
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=ya1dii",
|
||||
},
|
||||
{
|
||||
name: "Anna S.",
|
||||
role: "Architect",
|
||||
quote: "My morning ritual wouldn't be complete without their signature dark roast.",
|
||||
rating: 5,
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=ebfzkx",
|
||||
},
|
||||
{
|
||||
name: "Michael L.",
|
||||
role: "Professor",
|
||||
quote: "Finally, a place that takes their sourcing as seriously as the flavor.",
|
||||
rating: 5,
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=wwc8ym",
|
||||
},
|
||||
{
|
||||
name: "Sarah W.",
|
||||
role: "Writer",
|
||||
quote: "The perfect nook for getting work done while sipping on excellent pour-overs.",
|
||||
rating: 5,
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=098qhk",
|
||||
},
|
||||
{
|
||||
name: "David T.",
|
||||
role: "Photographer",
|
||||
quote: "Their seasonal roasts are always a beautiful surprise. Highly recommended.",
|
||||
rating: 5,
|
||||
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=8gwehs",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="social-proof" data-section="social-proof">
|
||||
<SocialProofMarquee
|
||||
tag="Certified Quality"
|
||||
title="Featured By Experts"
|
||||
description="Proudly recognized by industry leaders and coffee associations worldwide."
|
||||
names={[
|
||||
"Specialty Coffee Assoc.",
|
||||
"Organic Certification",
|
||||
"Fair Trade Intl.",
|
||||
"Barista Guild",
|
||||
"Gourmet Food Award",
|
||||
"Sustainable Farming",
|
||||
"Cafe Enthusiast Magazine",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="faq" data-section="faq">
|
||||
<FaqSplitMedia
|
||||
tag="Questions?"
|
||||
title="The Laude Routine"
|
||||
description="Everything you need to know about our beans, process, and shop."
|
||||
items={[
|
||||
{
|
||||
question: "Where are your beans sourced?",
|
||||
answer: "We source from high-altitude estates in Ethiopia, Colombia, and Indonesia through direct partnerships.",
|
||||
},
|
||||
{
|
||||
question: "Do you offer wholesale?",
|
||||
answer: "Yes, we work with restaurants and offices looking for sustainable, specialty-grade coffee.",
|
||||
},
|
||||
{
|
||||
question: "Are your pastries vegan?",
|
||||
answer: "We offer a rotating selection of plant-based pastries daily, clearly labeled in-store.",
|
||||
},
|
||||
{
|
||||
question: "Can I buy beans online?",
|
||||
answer: "Absolutely! Visit our online shop to have fresh roasted beans delivered to your doorstep.",
|
||||
},
|
||||
]}
|
||||
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=n0ueor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="contact" data-section="contact">
|
||||
<ContactSplitEmail
|
||||
tag="Join Us"
|
||||
title="Visit Laude Today"
|
||||
description="Subscribe for weekly roasting updates and occasional in-store event invitations."
|
||||
inputPlaceholder="your.email@example.com"
|
||||
buttonText="Join the Club"
|
||||
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=ommhco"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBrandReveal
|
||||
brand="Laude Coffee"
|
||||
columns={[
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: "About Us",
|
||||
href: "#about",
|
||||
},
|
||||
{
|
||||
label: "Our Roasts",
|
||||
href: "#features",
|
||||
},
|
||||
{
|
||||
label: "Careers",
|
||||
href: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: "Wholesale",
|
||||
href: "#",
|
||||
},
|
||||
{
|
||||
label: "Privacy Policy",
|
||||
href: "#",
|
||||
},
|
||||
{
|
||||
label: "Terms of Service",
|
||||
href: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: "Instagram",
|
||||
href: "https://instagram.com",
|
||||
},
|
||||
{
|
||||
label: "Twitter",
|
||||
href: "https://twitter.com",
|
||||
},
|
||||
{
|
||||
label: "Contact",
|
||||
href: "#contact",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
61
src/components/sections/about/AboutTestimonial.tsx
Normal file
61
src/components/sections/about/AboutTestimonial.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Quote } from "lucide-react";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
|
||||
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 aria-label="Testimonial section" className="py-20">
|
||||
<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 md:px-12 md:py-20 card rounded">
|
||||
<div className="absolute flex items-center justify-center -top-7 -left-7 md:-top-8 md:-left-8 size-14 md:size-16 primary-button rounded">
|
||||
<Quote className="h-5/10 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-col justify-center gap-5 py-8 md:py-5 h-full">
|
||||
<span className="w-fit px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={quote}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="text-3xl md:text-4xl font-medium leading-tight"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">{author}</span>
|
||||
<span className="text-accent">•</span>
|
||||
<span className="text-base text-foreground/75">{role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="md:col-span-2 aspect-square md:aspect-auto md:h-full card rounded overflow-hidden"
|
||||
>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutTestimonial;
|
||||
103
src/components/sections/contact/ContactSplitEmail.tsx
Normal file
103
src/components/sections/contact/ContactSplitEmail.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
|
||||
type ContactSplitEmailProps = {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
inputPlaceholder: string;
|
||||
buttonText: string;
|
||||
termsText?: string;
|
||||
onSubmit?: (email: string) => void;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const ContactSplitEmail = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
inputPlaceholder,
|
||||
buttonText,
|
||||
termsText,
|
||||
onSubmit,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: ContactSplitEmailProps) => {
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (onSubmit) {
|
||||
onSubmit(email);
|
||||
setEmail("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="Contact section" className="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">
|
||||
<div className="flex flex-col items-center w-full gap-3 px-5">
|
||||
<span className="card rounded px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-5xl md:text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-8/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
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"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
placeholder={inputPlaceholder}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
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"
|
||||
aria-label="Email address"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="primary-button h-9 px-5 text-sm rounded text-primary-cta-text cursor-pointer"
|
||||
>
|
||||
{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 overflow-hidden">
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSplitEmail;
|
||||
130
src/components/sections/faq/FaqSplitMedia.tsx
Normal file
130
src/components/sections/faq/FaqSplitMedia.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
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) => {
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
setActiveIndex(activeIndex === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="FAQ section" className="py-20">
|
||||
<div className="w-content-width mx-auto flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2">
|
||||
<span className="card rounded px-3 py-1 text-sm">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</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: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="card relative md:col-span-2 h-80 md:h-auto rounded overflow-hidden"
|
||||
>
|
||||
<ImageOrVideo
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.1 }}
|
||||
className="md:col-span-3 flex flex-col gap-3"
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="card rounded p-3 md:p-5 cursor-pointer select-none"
|
||||
onClick={() => handleToggle(index)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-base md:text-lg font-medium leading-tight">{item.question}</h3>
|
||||
<div className="flex shrink-0 items-center justify-center size-7 md:size-8 rounded primary-button">
|
||||
<Plus
|
||||
className={cls(
|
||||
"size-3.5 md:size-4 text-primary-cta-text transition-transform duration-300",
|
||||
activeIndex === index && "rotate-45"
|
||||
)}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{activeIndex === index && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<p className="pt-2 text-sm leading-relaxed opacity-75">{item.answer}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqSplitMedia;
|
||||
105
src/components/sections/features/FeaturesRevealCards.tsx
Normal file
105
src/components/sections/features/FeaturesRevealCards.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { motion } from "motion/react";
|
||||
import { Info } from "lucide-react";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
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 aria-label="Features section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
|
||||
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<GridOrCarousel>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.title} className="group relative overflow-hidden aspect-6/7 rounded">
|
||||
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
|
||||
|
||||
<div className="absolute top-5 left-5 z-20 perspective-[1000px]">
|
||||
<div className="relative size-8 transform-3d transition-transform duration-400 group-hover:rotate-y-180">
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden">
|
||||
<span className="text-sm font-medium text-foreground">{index + 1}</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-background backface-hidden rotate-y-180">
|
||||
<Info className="h-1/2 w-1/2 text-foreground" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 p-1">
|
||||
<div className="relative flex flex-col gap-1 p-3">
|
||||
<div className="absolute inset-0 -z-10 card rounded 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>
|
||||
</div>
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesRevealCards;
|
||||
101
src/components/sections/footer/FooterBrandReveal.tsx
Normal file
101
src/components/sections/footer/FooterBrandReveal.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useButtonClick } from "@/hooks/useButtonClick";
|
||||
import AutoFillText from "@/components/ui/AutoFillText";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type FooterLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type FooterColumn = {
|
||||
items: FooterLink[];
|
||||
};
|
||||
|
||||
const FooterLinkItem = ({ label, href, onClick }: FooterLink) => {
|
||||
const handleClick = useButtonClick(href, onClick);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-base">
|
||||
<ChevronRight className="size-4" strokeWidth={3} aria-hidden="true" />
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="text-base text-primary-cta-text font-medium hover:opacity-75 transition-opacity cursor-pointer"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FooterBrandReveal = ({
|
||||
brand,
|
||||
columns,
|
||||
}: {
|
||||
brand: string;
|
||||
columns: FooterColumn[];
|
||||
}) => {
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
const [footerHeight, setFooterHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeight = () => {
|
||||
if (footerRef.current) {
|
||||
setFooterHeight(footerRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
if (footerRef.current) {
|
||||
resizeObserver.observe(footerRef.current);
|
||||
}
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative z-0 w-full mt-20"
|
||||
style={{
|
||||
height: footerHeight ? `${footerHeight}px` : "auto",
|
||||
clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="fixed bottom-0 w-full"
|
||||
style={{ height: footerHeight ? `${footerHeight}px` : "auto" }}
|
||||
>
|
||||
<footer
|
||||
ref={footerRef}
|
||||
aria-label="Site footer"
|
||||
className="w-full py-15 rounded-t-lg overflow-hidden primary-button text-primary-cta-text"
|
||||
>
|
||||
<div className="w-content-width mx-auto flex flex-col gap-10 md:gap-20">
|
||||
<AutoFillText className="font-medium">{brand}</AutoFillText>
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"flex flex-col gap-8 mb-10 md:flex-row",
|
||||
columns.length === 1 ? "md:justify-center" : "md:justify-between"
|
||||
)}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<div key={index} className="flex flex-col items-start gap-3">
|
||||
{column.items.map((item) => (
|
||||
<FooterLinkItem key={item.label} label={item.label} href={item.href} onClick={item.onClick} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterBrandReveal;
|
||||
62
src/components/sections/hero/HeroBillboard.tsx
Normal file
62
src/components/sections/hero/HeroBillboard.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
|
||||
type HeroBillboardProps = {
|
||||
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 HeroBillboard = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
}: HeroBillboardProps) => {
|
||||
return (
|
||||
<section aria-label="Hero section" className="pt-25 pb-20 md:py-30">
|
||||
<div className="flex flex-col gap-10 md:gap-13 w-content-width mx-auto">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h1"
|
||||
className="text-6xl font-medium text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="text-base md:text-lg leading-tight text-balance"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap max-md:justify-center gap-3 mt-2">
|
||||
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
|
||||
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
|
||||
className="w-full p-5 card rounded overflow-hidden"
|
||||
>
|
||||
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-square md:aspect-video" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroBillboard;
|
||||
62
src/components/sections/metrics/MetricsMinimalCards.tsx
Normal file
62
src/components/sections/metrics/MetricsMinimalCards.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { motion } from "motion/react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
|
||||
type Metric = {
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const MetricsMinimalCards = ({
|
||||
tag,
|
||||
title,
|
||||
metrics,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
metrics: Metric[];
|
||||
}) => (
|
||||
<section aria-label="Metrics section" className="py-20">
|
||||
<div className="flex flex-col gap-8 w-content-width mx-auto">
|
||||
<div className="flex flex-col gap-5">
|
||||
<span className="px-3 py-1 w-fit text-sm card rounded md:hidden">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-3xl md:text-5xl font-medium leading-tight text-balance"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-px bg-accent" />
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-8">
|
||||
<span className="hidden md:block px-3 py-1 text-sm card rounded">{tag}</span>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className={cls(
|
||||
"grid grid-cols-1 gap-5 flex-1",
|
||||
metrics.length >= 2 && "md:grid-cols-2"
|
||||
)}
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.value} className="flex flex-col justify-between gap-5 p-5 md:p-8 aspect-video card rounded">
|
||||
<span className="text-6xl md:text-8xl font-medium leading-none truncate">{metric.value}</span>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="w-full h-px bg-accent" />
|
||||
<p className="text-base md:text-lg leading-tight text-balance">{metric.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export default MetricsMinimalCards;
|
||||
68
src/components/sections/social-proof/SocialProofMarquee.tsx
Normal file
68
src/components/sections/social-proof/SocialProofMarquee.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { motion } from "motion/react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
|
||||
const SocialProofMarquee = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
names,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
names: string[];
|
||||
}) => {
|
||||
return (
|
||||
<section aria-label="Social proof section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2 w-content-width mx-auto">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className="w-content-width mx-auto overflow-hidden mask-fade-x"
|
||||
>
|
||||
<div className="flex w-max animate-marquee-horizontal" style={{ animationDuration: "45s" }}>
|
||||
{[...names, ...names, ...names, ...names].map((name, index) => (
|
||||
<div key={index} className="shrink-0 mx-3 px-5 py-3 rounded card">
|
||||
<span className="text-2xl font-semibold whitespace-nowrap opacity-75">{name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialProofMarquee;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Star } from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type Testimonial = {
|
||||
name: string;
|
||||
role: string;
|
||||
quote: string;
|
||||
rating: number;
|
||||
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
|
||||
|
||||
const TestimonialRatingCards = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
testimonials,
|
||||
}: {
|
||||
tag: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryButton?: { text: string; href: string };
|
||||
secondaryButton?: { text: string; href: string };
|
||||
testimonials: Testimonial[];
|
||||
}) => {
|
||||
return (
|
||||
<section aria-label="Testimonials section" className="py-20">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col items-center gap-3 md:gap-2 w-content-width mx-auto">
|
||||
<span className="px-3 py-1 text-sm rounded card">{tag}</span>
|
||||
|
||||
<TextAnimation
|
||||
text={title}
|
||||
variant="slide-up"
|
||||
tag="h2"
|
||||
className="text-6xl font-medium text-center text-balance"
|
||||
/>
|
||||
|
||||
<TextAnimation
|
||||
text={description}
|
||||
variant="slide-up"
|
||||
tag="p"
|
||||
className="md:max-w-6/10 text-lg leading-tight text-center"
|
||||
/>
|
||||
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="flex flex-wrap justify-center gap-3 mt-1 md:mt-2">
|
||||
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
|
||||
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<GridOrCarousel carouselThreshold={3}>
|
||||
{testimonials.map((testimonial) => (
|
||||
<div key={testimonial.name} className="flex flex-col justify-between gap-5 h-full p-5 rounded card">
|
||||
<div className="flex flex-col items-start gap-5">
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Star
|
||||
key={index}
|
||||
className={cls("size-5 text-accent", index < testimonial.rating ? "fill-accent" : "fill-transparent")}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-lg leading-tight">{testimonial.quote}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 overflow-hidden rounded-full">
|
||||
<ImageOrVideo imageSrc={testimonial.imageSrc} videoSrc={testimonial.videoSrc} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-medium leading-tight">{testimonial.name}</span>
|
||||
<span className="text-sm leading-tight opacity-75">{testimonial.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</GridOrCarousel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialRatingCards;
|
||||
46
src/components/ui/AnimatedBarChart.tsx
Normal file
46
src/components/ui/AnimatedBarChart.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const BARS = [
|
||||
{ height: 100, hoverHeight: 40 },
|
||||
{ height: 84, hoverHeight: 100 },
|
||||
{ height: 62, hoverHeight: 75 },
|
||||
{ height: 90, hoverHeight: 50 },
|
||||
{ height: 70, hoverHeight: 90 },
|
||||
{ height: 50, hoverHeight: 60 },
|
||||
{ height: 75, hoverHeight: 85 },
|
||||
{ height: 80, hoverHeight: 70 },
|
||||
];
|
||||
|
||||
const AnimatedBarChart = () => {
|
||||
const [active, setActive] = useState(2);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setActive((p) => (p + 1) % BARS.length), 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden md:block h-full w-full"
|
||||
style={{ maskImage: "linear-gradient(to bottom, black 40%, transparent)" }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className="flex items-end gap-4 h-full w-full">
|
||||
{BARS.map((bar, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative w-full rounded bg-background-accent transition-all duration-500"
|
||||
style={{ height: `${isHovered ? bar.hoverHeight : bar.height}%` }}
|
||||
>
|
||||
<div className={cls("absolute inset-0 rounded primary-button transition-opacity duration-500", active === i ? "opacity-100" : "opacity-0")} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedBarChart;
|
||||
67
src/components/ui/AutoFillText.tsx
Normal file
67
src/components/ui/AutoFillText.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const AutoFillText = ({
|
||||
children,
|
||||
className = "",
|
||||
paddingY = "py-10",
|
||||
}: {
|
||||
children: string;
|
||||
className?: string;
|
||||
paddingY?: string;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLHeadingElement>(null);
|
||||
const [fontSize, setFontSize] = useState<number | null>(null);
|
||||
|
||||
const hasDescenders = /[gjpqy]/.test(children);
|
||||
const lineHeight = hasDescenders ? 1.2 : 0.8;
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const text = textRef.current;
|
||||
if (!container || !text) return;
|
||||
|
||||
const calculateSize = () => {
|
||||
const containerWidth = container.offsetWidth;
|
||||
if (containerWidth === 0) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const styles = getComputedStyle(text);
|
||||
ctx.font = `${styles.fontWeight} 100px ${styles.fontFamily}`;
|
||||
const textWidth = ctx.measureText(children).width;
|
||||
|
||||
if (textWidth > 0) {
|
||||
setFontSize((containerWidth / textWidth) * 100);
|
||||
}
|
||||
};
|
||||
|
||||
calculateSize();
|
||||
|
||||
const observer = new ResizeObserver(calculateSize);
|
||||
observer.observe(container);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cls("w-full min-w-0 flex-1", !hasDescenders && paddingY)}>
|
||||
<h2
|
||||
ref={textRef}
|
||||
className={cls(
|
||||
"whitespace-nowrap transition-opacity duration-150",
|
||||
fontSize ? "opacity-100" : "opacity-0",
|
||||
className
|
||||
)}
|
||||
style={{ fontSize: fontSize ? `${fontSize}px` : undefined, lineHeight }}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoFillText;
|
||||
44
src/components/ui/Button.tsx
Normal file
44
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useButtonClick } from "@/hooks/useButtonClick";
|
||||
|
||||
interface ButtonProps {
|
||||
text: string;
|
||||
variant?: "primary" | "secondary";
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
animate?: boolean;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Button = ({ text, variant = "primary", href, onClick, animate = false, delay = 0, className = "" }: ButtonProps) => {
|
||||
const handleClick = useButtonClick(href, onClick);
|
||||
|
||||
const classes = cls(
|
||||
"flex items-center justify-center h-9 px-6 text-sm rounded cursor-pointer",
|
||||
variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text",
|
||||
className
|
||||
);
|
||||
|
||||
const button = href
|
||||
? <a href={href} onClick={handleClick} className={classes}>{text}</a>
|
||||
: <button onClick={handleClick} className={classes}>{text}</button>;
|
||||
|
||||
if (!animate) return button;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.6, delay, ease: "easeOut" }}
|
||||
>
|
||||
{button}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
43
src/components/ui/ChatMarquee.tsx
Normal file
43
src/components/ui/ChatMarquee.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Send } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type Exchange = { userMessage: string; aiResponse: string };
|
||||
|
||||
const ChatMarquee = ({ aiIcon: AiIcon, userIcon: UserIcon, exchanges, placeholder }: { aiIcon: LucideIcon; userIcon: LucideIcon; exchanges: Exchange[]; placeholder: string }) => {
|
||||
const messages = exchanges.flatMap((e) => [{ content: e.userMessage, isUser: true }, { content: e.aiResponse, isUser: false }]);
|
||||
const duplicated = [...messages, ...messages];
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col h-full w-full overflow-hidden">
|
||||
<div className="flex-1 overflow-hidden mask-fade-y">
|
||||
<div className="flex flex-col px-4 animate-marquee-vertical">
|
||||
{duplicated.map((msg, i) => (
|
||||
<div key={i} className={cls("flex items-end gap-2 mb-4 shrink-0", msg.isUser ? "flex-row-reverse" : "flex-row")}>
|
||||
{msg.isUser ? (
|
||||
<div className="flex items-center justify-center h-8 w-8 primary-button rounded shrink-0">
|
||||
<UserIcon className="h-3 w-3 text-primary-cta-text" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-8 w-8 card rounded shrink-0">
|
||||
<AiIcon className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
<div className={cls("max-w-3/4 px-4 py-3 text-sm leading-tight", msg.isUser ? "primary-button rounded-2xl rounded-br-none text-primary-cta-text" : "card rounded-2xl rounded-bl-none")}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 pl-4 card rounded">
|
||||
<p className="flex-1 text-sm text-foreground/75 truncate">{placeholder}</p>
|
||||
<div className="flex items-center justify-center h-7 w-7 primary-button rounded">
|
||||
<Send className="h-3 w-3 text-primary-cta-text" strokeWidth={1.75} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMarquee;
|
||||
47
src/components/ui/ChecklistTimeline.tsx
Normal file
47
src/components/ui/ChecklistTimeline.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Check, Loader } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type Item = { label: string; detail: string };
|
||||
|
||||
const DELAYS = [
|
||||
["delay-150", "delay-200", "delay-[250ms]"],
|
||||
["delay-[350ms]", "delay-[400ms]", "delay-[450ms]"],
|
||||
["delay-[550ms]", "delay-[600ms]", "delay-[650ms]"],
|
||||
];
|
||||
|
||||
const ChecklistTimeline = ({ heading, subheading, items, completedLabel }: { heading: string; subheading: string; items: [Item, Item, Item]; completedLabel: string }) => (
|
||||
<div className="group relative flex items-center justify-center h-full w-full overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{[1, 0.8, 0.6].map((s) => <div key={s} className="absolute h-full aspect-square rounded-full border border-background-accent/30" style={{ transform: `scale(${s})` }} />)}
|
||||
</div>
|
||||
<div className="relative flex flex-col gap-3 p-4 max-w-full w-8/10 mask-fade-y">
|
||||
<div className="flex items-center gap-2 p-3 card shadow rounded">
|
||||
<Loader className="h-4 w-4 text-primary transition-transform duration-1000 group-hover:rotate-360" strokeWidth={1.5} />
|
||||
<p className="text-xs truncate">{heading}</p>
|
||||
<p className="text-xs text-foreground/75 ml-auto whitespace-nowrap">{subheading}</p>
|
||||
</div>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 card shadow rounded">
|
||||
<div className="relative flex items-center justify-center h-6 w-6 card shadow rounded">
|
||||
<div className="absolute h-2 w-2 primary-button rounded transition-opacity duration-300 group-hover:opacity-0" />
|
||||
<div className={cls("absolute inset-0 flex items-center justify-center primary-button rounded opacity-0 scale-75 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100", DELAYS[i][0])}>
|
||||
<Check className="h-3 w-3 text-primary-cta-text" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between gap-4 min-w-0">
|
||||
<p className={cls("text-xs truncate opacity-0 transition-opacity duration-300 group-hover:opacity-100", DELAYS[i][1])}>{item.label}</p>
|
||||
<p className={cls("text-xs text-foreground/75 whitespace-nowrap opacity-0 translate-y-1 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0", DELAYS[i][2])}>{item.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="relative flex items-center justify-center p-3 primary-button rounded">
|
||||
<div className="absolute flex gap-2 transition-opacity duration-500 delay-900 group-hover:opacity-0">
|
||||
{[0, 1, 2].map((j) => <div key={j} className="h-2 w-2 rounded bg-primary-cta-text" />)}
|
||||
</div>
|
||||
<p className="text-xs text-primary-cta-text truncate opacity-0 transition-opacity duration-500 delay-900 group-hover:opacity-100">{completedLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ChecklistTimeline;
|
||||
64
src/components/ui/GridOrCarousel.tsx
Normal file
64
src/components/ui/GridOrCarousel.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Children, type ReactNode } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useCarouselControls } from "@/hooks/useCarouselControls";
|
||||
|
||||
interface GridOrCarouselProps {
|
||||
children: ReactNode;
|
||||
carouselThreshold?: 2 | 3 | 4;
|
||||
}
|
||||
|
||||
const GridOrCarousel = ({ children, carouselThreshold = 4 }: GridOrCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true });
|
||||
const { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
|
||||
|
||||
const items = Children.toArray(children);
|
||||
const count = items.length;
|
||||
|
||||
if (count <= carouselThreshold) {
|
||||
return (
|
||||
<div className={cls(
|
||||
"grid grid-cols-1 gap-5 mx-auto w-content-width",
|
||||
count === 2 && "md:grid-cols-2",
|
||||
count === 3 && "md:grid-cols-3",
|
||||
count === 4 && "md:grid-cols-4"
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 w-full">
|
||||
<div ref={emblaRef} className="overflow-hidden w-full cursor-grab">
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-carousel-padding" />
|
||||
{items.map((child, i) => (
|
||||
<div key={i} className={cls("shrink-0", carouselThreshold === 2 ? "w-carousel-item-2" : carouselThreshold === 3 ? "w-carousel-item-3" : "w-carousel-item-4")}>{child}</div>
|
||||
))}
|
||||
<div className="shrink-0 w-carousel-padding" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<div className="shrink-0 w-carousel-padding-controls" />
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="relative h-2 w-1/2 card rounded overflow-hidden">
|
||||
<div className="absolute top-0 bottom-0 -left-full w-full primary-button rounded" style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }} />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={scrollPrev} disabled={prevDisabled} type="button" aria-label="Previous" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
|
||||
<ChevronLeft className="h-2/5 aspect-square text-secondary-cta-text" />
|
||||
</button>
|
||||
<button onClick={scrollNext} disabled={nextDisabled} type="button" aria-label="Next" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
|
||||
<ChevronRight className="h-2/5 aspect-square text-secondary-cta-text" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 w-carousel-padding-controls" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridOrCarousel;
|
||||
61
src/components/ui/HoverPattern.tsx
Normal file
61
src/components/ui/HoverPattern.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { motion, useMotionValue, useMotionTemplate } from "motion/react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const randomChars = () => Array.from({ length: 1500 }, () => CHARS[Math.floor(Math.random() * 62)]).join("");
|
||||
|
||||
interface HoverPatternProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const HoverPattern = ({ children, className = "" }: HoverPatternProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const [chars, setChars] = useState(randomChars);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile && ref.current) {
|
||||
x.set(ref.current.offsetWidth / 2);
|
||||
y.set(ref.current.offsetHeight / 2);
|
||||
}
|
||||
}, [isMobile, x, y]);
|
||||
|
||||
const mask = useMotionTemplate`radial-gradient(${isMobile ? 110 : 250}px at ${x}px ${y}px, white, transparent)`;
|
||||
const base = "absolute inset-0 rounded-[inherit] transition-opacity duration-300";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cls("group/pattern relative", className)}
|
||||
onMouseMove={isMobile ? undefined : (e) => {
|
||||
if (!ref.current) return;
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
x.set(e.clientX - rect.left);
|
||||
y.set(e.clientY - rect.top);
|
||||
setChars(randomChars());
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div className="pointer-events-none absolute inset-0 rounded-[inherit]">
|
||||
<div className={cls(base, isMobile ? "opacity-25" : "opacity-0 group-hover/pattern:opacity-25")} style={{ background: "linear-gradient(white, transparent)" }} />
|
||||
<motion.div className={cls(base, "bg-linear-to-r from-accent to-accent/50 backdrop-blur-xl", isMobile ? "opacity-100" : "opacity-0 group-hover/pattern:opacity-100")} style={{ maskImage: mask, WebkitMaskImage: mask }} />
|
||||
<motion.div className={cls(base, "mix-blend-overlay", isMobile ? "opacity-100" : "opacity-0 group-hover/pattern:opacity-100")} style={{ maskImage: mask, WebkitMaskImage: mask }}>
|
||||
<p className="absolute inset-0 h-full whitespace-pre-wrap wrap-break-word font-mono text-xs font-bold text-white">{chars}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HoverPattern;
|
||||
27
src/components/ui/IconTextMarquee.tsx
Normal file
27
src/components/ui/IconTextMarquee.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const IconTextMarquee = ({ centerIcon: CenterIcon, texts }: { centerIcon: LucideIcon; texts: string[] }) => {
|
||||
const items = [...texts, ...texts];
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col h-full w-full overflow-hidden" style={{ maskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)" }}>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col gap-2 w-full opacity-60">
|
||||
{Array.from({ length: 10 }).map((_, row) => (
|
||||
<div key={row} className={cls("flex gap-2", row % 2 === 0 ? "animate-marquee-horizontal" : "animate-marquee-horizontal-reverse")}>
|
||||
{items.map((text, i) => (
|
||||
<div key={i} className="flex items-center justify-center px-4 py-2 card rounded">
|
||||
<p className="text-sm leading-tight">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex items-center justify-center h-16 w-16 primary-button backdrop-blur-sm rounded">
|
||||
<CenterIcon className="h-6 w-6 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconTextMarquee;
|
||||
41
src/components/ui/ImageOrVideo.tsx
Normal file
41
src/components/ui/ImageOrVideo.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ImageOrVideoProps {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ImageOrVideo = ({
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
className = "",
|
||||
}: ImageOrVideoProps) => {
|
||||
if (videoSrc) {
|
||||
return (
|
||||
<video
|
||||
src={videoSrc}
|
||||
aria-label={videoSrc}
|
||||
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (imageSrc) {
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageSrc}
|
||||
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ImageOrVideo;
|
||||
27
src/components/ui/InfoCardMarquee.tsx
Normal file
27
src/components/ui/InfoCardMarquee.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
type Item = { icon: LucideIcon; label: string; value: string };
|
||||
|
||||
const InfoCardMarquee = ({ items }: { items: Item[] }) => {
|
||||
const duplicated = [...items, ...items, ...items, ...items];
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-hidden mask-fade-y">
|
||||
<div className="flex flex-col animate-marquee-vertical">
|
||||
{duplicated.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between gap-4 p-3 mb-4 card rounded">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center h-10 w-10 secondary-button rounded">
|
||||
<item.icon className="h-4 w-4 text-secondary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-base truncate">{item.label}</p>
|
||||
</div>
|
||||
<p className="text-base">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoCardMarquee;
|
||||
76
src/components/ui/LoopCarousel.tsx
Normal file
76
src/components/ui/LoopCarousel.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Children, useCallback, useEffect, useState, type ReactNode } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import type { EmblaCarouselType } from "embla-carousel";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface LoopCarouselProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const LoopCarousel = ({ children }: LoopCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const items = Children.toArray(children);
|
||||
|
||||
const onSelect = useCallback((api: EmblaCarouselType) => {
|
||||
setSelectedIndex(api.selectedScrollSnap());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
|
||||
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
onSelect(emblaApi);
|
||||
emblaApi.on("select", onSelect).on("reInit", onSelect);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", onSelect).off("reInit", onSelect);
|
||||
};
|
||||
}, [emblaApi, onSelect]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full md:w-content-width mx-auto">
|
||||
<div ref={emblaRef} className="overflow-hidden w-full mask-fade-x">
|
||||
<div className="flex w-full">
|
||||
{items.map((child, index) => (
|
||||
<div key={index} className="shrink-0 w-content-width md:w-[clamp(18rem,50vw,48rem)] mr-3 md:mr-6">
|
||||
<div
|
||||
className={cls(
|
||||
"transition-all duration-500 ease-out",
|
||||
selectedIndex === index ? "opacity-100 scale-100" : "opacity-70 scale-90"
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-y-0 left-0 right-0 flex items-center justify-between w-content-width mx-auto pointer-events-none">
|
||||
<button
|
||||
onClick={scrollPrev}
|
||||
type="button"
|
||||
aria-label="Previous slide"
|
||||
className="flex items-center justify-center h-8 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
|
||||
>
|
||||
<ChevronLeft className="h-2/5 aspect-square text-primary-cta-text" />
|
||||
</button>
|
||||
<button
|
||||
onClick={scrollNext}
|
||||
type="button"
|
||||
aria-label="Next slide"
|
||||
className="flex items-center justify-center h-8 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
|
||||
>
|
||||
<ChevronRight className="h-2/5 aspect-square text-primary-cta-text" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopCarousel;
|
||||
32
src/components/ui/MediaStack.tsx
Normal file
32
src/components/ui/MediaStack.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type Item = { imageSrc?: string; videoSrc?: string };
|
||||
|
||||
const MediaStack = ({ items }: { items: [Item, Item, Item] }) => (
|
||||
<div className="group/stack relative flex items-center justify-center h-full w-full rounded select-none card">
|
||||
<div className={cls(
|
||||
"absolute z-1 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
|
||||
"translate-x-[12%] -translate-y-[8%] rotate-8 transition-all duration-500",
|
||||
"group-hover/stack:translate-x-[22%] group-hover/stack:-translate-y-[14%] group-hover/stack:rotate-12"
|
||||
)}>
|
||||
<ImageOrVideo imageSrc={items[2].imageSrc} videoSrc={items[2].videoSrc} className="h-full rounded" />
|
||||
</div>
|
||||
<div className={cls(
|
||||
"absolute z-2 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
|
||||
"-translate-x-[12%] -translate-y-[8%] -rotate-8 transition-all duration-500",
|
||||
"group-hover/stack:-translate-x-[22%] group-hover/stack:-translate-y-[14%] group-hover/stack:-rotate-12"
|
||||
)}>
|
||||
<ImageOrVideo imageSrc={items[1].imageSrc} videoSrc={items[1].videoSrc} className="h-full rounded" />
|
||||
</div>
|
||||
<div className={cls(
|
||||
"absolute z-30 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
|
||||
"translate-y-[10%] transition-all duration-500",
|
||||
"group-hover/stack:translate-y-[20%]"
|
||||
)}>
|
||||
<ImageOrVideo imageSrc={items[0].imageSrc} videoSrc={items[0].videoSrc} className="h-full rounded" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MediaStack;
|
||||
122
src/components/ui/NavbarCentered.tsx
Normal file
122
src/components/ui/NavbarCentered.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { Plus, ArrowRight } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
interface NavbarCenteredProps {
|
||||
logo: string;
|
||||
navItems: { name: string; href: string }[];
|
||||
ctaButton: { text: string; href: string };
|
||||
}
|
||||
|
||||
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
|
||||
if (href.startsWith("#")) {
|
||||
e.preventDefault();
|
||||
const element = document.getElementById(href.slice(1));
|
||||
element?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const NavbarCentered = ({ logo, navItems, ctaButton }: NavbarCenteredProps) => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setIsScrolled(window.scrollY > 50);
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className={cls(
|
||||
"fixed z-1000 top-0 left-0 w-full transition-all duration-500 ease-in-out",
|
||||
isScrolled ? "h-15 bg-background/80 backdrop-blur-sm" : "h-20 bg-background/0 backdrop-blur-0"
|
||||
)}
|
||||
>
|
||||
<div className="relative flex items-center justify-between h-full w-content-width mx-auto">
|
||||
<a href="/" className="text-xl font-medium text-foreground">{logo}</a>
|
||||
|
||||
<div className="hidden md:flex absolute left-1/2 items-center gap-6 -translate-x-1/2">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item.href)}
|
||||
className="text-base text-foreground hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex md:hidden items-center justify-center shrink-0 h-8 w-8 bg-foreground rounded cursor-pointer"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
<Plus
|
||||
className={cls("w-1/2 h-1/2 text-background transition-transform duration-300", menuOpen ? "rotate-45" : "rotate-0")}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<AnimatePresence>
|
||||
{menuOpen && (
|
||||
<motion.div
|
||||
initial={{ y: "-135%" }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: "-135%" }}
|
||||
transition={{ type: "spring", damping: 26, stiffness: 170 }}
|
||||
className="md:hidden fixed z-1000 top-3 left-3 right-3 p-6 card rounded"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<p className="text-xl text-foreground">Menu</p>
|
||||
<button
|
||||
className="flex items-center justify-center shrink-0 h-8 w-8 bg-foreground rounded cursor-pointer"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<Plus className="w-1/2 h-1/2 text-background rotate-45" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{navItems.map((item, index) => (
|
||||
<div key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
|
||||
className="flex items-center justify-between py-2 text-base font-medium text-foreground"
|
||||
>
|
||||
{item.name}
|
||||
<ArrowRight className="h-4 w-4 text-foreground" strokeWidth={1.5} />
|
||||
</a>
|
||||
{index < navItems.length - 1 && (
|
||||
<div className="h-px bg-linear-to-r from-transparent via-foreground/20 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" className="w-full" />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarCentered;
|
||||
30
src/components/ui/OrbitingIcons.tsx
Normal file
30
src/components/ui/OrbitingIcons.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
const OrbitingIcons = ({ centerIcon: CenterIcon, items }: { centerIcon: LucideIcon; items: LucideIcon[] }) => (
|
||||
<div
|
||||
className="relative flex items-center justify-center h-full overflow-hidden"
|
||||
style={{ perspective: "2000px", maskImage: "linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)", maskComposite: "intersect" }}
|
||||
>
|
||||
<div className="flex items-center justify-center w-full h-full" style={{ transform: "rotateY(20deg) rotateX(20deg) rotateZ(-20deg)" }}>
|
||||
<div className="absolute h-60 w-60 opacity-85 border border-background-accent shadow rounded-full" />
|
||||
<div className="absolute h-80 w-80 opacity-75 border border-background-accent shadow rounded-full" />
|
||||
<div className="absolute h-100 w-100 opacity-65 border border-background-accent shadow rounded-full" />
|
||||
<div className="absolute flex items-center justify-center h-40 w-40 border border-background-accent shadow rounded-full">
|
||||
<div className="flex items-center justify-center h-20 w-20 primary-button rounded-full">
|
||||
<CenterIcon className="h-10 w-10 text-primary-cta-text" strokeWidth={1.25} />
|
||||
</div>
|
||||
{items.map((Icon, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute flex items-center justify-center h-10 w-10 rounded shadow card -ml-5 -mt-5"
|
||||
style={{ top: "50%", left: "50%", animation: "orbit 12s linear infinite", "--initial-position": `${(360 / items.length) * i}deg`, "--translate-position": "160px" } as React.CSSProperties}
|
||||
>
|
||||
<Icon className="h-4 w-4" strokeWidth={1.5} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default OrbitingIcons;
|
||||
60
src/components/ui/TextAnimation.tsx
Normal file
60
src/components/ui/TextAnimation.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Variant = "slide-up" | "fade-blur" | "fade";
|
||||
|
||||
interface TextAnimationProps {
|
||||
text: string;
|
||||
variant: Variant;
|
||||
tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const VARIANTS = {
|
||||
"slide-up": {
|
||||
hidden: { opacity: 0, y: "50%" },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
},
|
||||
"fade-blur": {
|
||||
hidden: { opacity: 0, filter: "blur(10px)" },
|
||||
visible: { opacity: 1, filter: "blur(0px)" },
|
||||
},
|
||||
"fade": {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const EASING: Record<Variant, [number, number, number, number]> = {
|
||||
"slide-up": [0.25, 0.46, 0.45, 0.94],
|
||||
"fade-blur": [0.45, 0, 0.55, 1],
|
||||
"fade": [0.45, 0, 0.55, 1],
|
||||
};
|
||||
|
||||
const TextAnimation = ({ text, variant, tag = "p", className = "" }: TextAnimationProps) => {
|
||||
const Tag = motion[tag] as typeof motion.p;
|
||||
const words = text.split(" ");
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={className}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-20%" }}
|
||||
transition={{ staggerChildren: 0.04 }}
|
||||
>
|
||||
{words.map((word, i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className="inline-block"
|
||||
variants={VARIANTS[variant]}
|
||||
transition={{ duration: 0.6, ease: EASING[variant] }}
|
||||
>
|
||||
{word}
|
||||
{i < words.length - 1 && "\u00A0"}
|
||||
</motion.span>
|
||||
))}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextAnimation;
|
||||
28
src/components/ui/TiltedStackCards.tsx
Normal file
28
src/components/ui/TiltedStackCards.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type Item = { icon: LucideIcon; title: string; subtitle: string; detail: string };
|
||||
|
||||
const POS = ["-translate-y-14 hover:-translate-y-20", "translate-x-16 hover:-translate-y-4", "translate-x-32 translate-y-16 hover:translate-y-10"];
|
||||
|
||||
const TiltedStackCards = ({ items }: { items: [Item, Item, Item] }) => (
|
||||
<div
|
||||
className="h-full grid place-items-center [grid-template-areas:'stack']"
|
||||
style={{ maskImage: "linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, black, black 80%, transparent)", maskComposite: "intersect" }}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className={cls("flex flex-col justify-between gap-2 p-6 w-80 h-36 card rounded transition-all duration-500 -skew-y-[8deg] [grid-area:stack] 2xl:w-90", POS[i])}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center h-5 w-5 rounded primary-button">
|
||||
<item.icon className="h-3 w-3 text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-base">{item.title}</p>
|
||||
</div>
|
||||
<p className="text-lg whitespace-nowrap">{item.subtitle}</p>
|
||||
<p className="text-base">{item.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TiltedStackCards;
|
||||
37
src/components/ui/Transition.tsx
Normal file
37
src/components/ui/Transition.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { motion } from "motion/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface TransitionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
transitionType?: "full" | "fade";
|
||||
whileInView?: boolean;
|
||||
}
|
||||
|
||||
const Transition = ({
|
||||
children,
|
||||
className = "flex flex-col w-full gap-6",
|
||||
transitionType = "full",
|
||||
whileInView = true,
|
||||
}: TransitionProps) => {
|
||||
const initial = transitionType === "full"
|
||||
? { opacity: 0, y: 20 }
|
||||
: { opacity: 0 };
|
||||
|
||||
const target = transitionType === "full"
|
||||
? { opacity: 1, y: 0 }
|
||||
: { opacity: 1 };
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={initial}
|
||||
{...(whileInView ? { whileInView: target, viewport: { once: true, margin: "-15%" } } : { animate: target })}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transition;
|
||||
51
src/hooks/useButtonClick.ts
Normal file
51
src/hooks/useButtonClick.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
export const useButtonClick = (href?: string, onClick?: () => void) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const scrollToElement = (sectionId: string, delay: number = 100) => {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (href) {
|
||||
const isExternalLink = /^(https?:\/\/|www\.)/.test(href);
|
||||
|
||||
if (isExternalLink) {
|
||||
window.open(
|
||||
href.startsWith("www.") ? `https://${href}` : href,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
} else if (href.startsWith("/")) {
|
||||
const [path, hash] = href.split("#");
|
||||
|
||||
if (path !== location.pathname) {
|
||||
navigate(path);
|
||||
if (hash) {
|
||||
setTimeout(() => {
|
||||
window.location.hash = hash;
|
||||
scrollToElement(hash, 100);
|
||||
}, 100);
|
||||
}
|
||||
} else if (hash) {
|
||||
window.location.hash = hash;
|
||||
scrollToElement(hash, 50);
|
||||
}
|
||||
} else if (href.startsWith("#")) {
|
||||
scrollToElement(href.slice(1), 50);
|
||||
} else {
|
||||
scrollToElement(href, 50);
|
||||
}
|
||||
}
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
return handleClick;
|
||||
};
|
||||
45
src/hooks/useCarouselControls.ts
Normal file
45
src/hooks/useCarouselControls.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { EmblaCarouselType } from "embla-carousel";
|
||||
|
||||
export const useCarouselControls = (emblaApi: EmblaCarouselType | undefined) => {
|
||||
const [prevDisabled, setPrevDisabled] = useState(true);
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
|
||||
const scrollPrev = useCallback(() => {
|
||||
if (!emblaApi) return;
|
||||
emblaApi.scrollPrev();
|
||||
}, [emblaApi]);
|
||||
|
||||
const scrollNext = useCallback(() => {
|
||||
if (!emblaApi) return;
|
||||
emblaApi.scrollNext();
|
||||
}, [emblaApi]);
|
||||
|
||||
const onSelect = useCallback((api: EmblaCarouselType) => {
|
||||
setPrevDisabled(!api.canScrollPrev());
|
||||
setNextDisabled(!api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const onScroll = useCallback((api: EmblaCarouselType) => {
|
||||
const progress = Math.max(0, Math.min(1, api.scrollProgress()));
|
||||
setScrollProgress(progress * 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
onSelect(emblaApi);
|
||||
onScroll(emblaApi);
|
||||
|
||||
emblaApi.on("reInit", onSelect).on("select", onSelect);
|
||||
emblaApi.on("reInit", onScroll).on("scroll", onScroll);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("reInit", onSelect).off("select", onSelect);
|
||||
emblaApi.off("reInit", onScroll).off("scroll", onScroll);
|
||||
};
|
||||
}, [emblaApi, onSelect, onScroll]);
|
||||
|
||||
return { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress };
|
||||
};
|
||||
172
src/index.css
Normal file
172
src/index.css
Normal file
@@ -0,0 +1,172 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=${publicSans.variable}:wght@400;500;600;700&display=swap');
|
||||
@import "tailwindcss";
|
||||
@import "./styles/masks.css";
|
||||
@import "./styles/animations.css";
|
||||
|
||||
:root {
|
||||
/* @colorThemes/lightTheme/grayBlueAccent */
|
||||
--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;
|
||||
|
||||
/* @layout/border-radius/rounded */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* @layout/content-width/medium */
|
||||
--width-content-width: clamp(40rem, 80vw, 100rem);
|
||||
|
||||
/* @utilities/masks */
|
||||
--vw-1_5: 1.5vw;
|
||||
--width-x-padding-mask-fade: 5vw;
|
||||
|
||||
/* @layout/carousel */
|
||||
--width-carousel-padding: calc((100vw - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls: calc((100vw - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-item-2: calc(var(--width-content-width) / 2 - var(--vw-1_5) / 2);
|
||||
--width-carousel-item-3: calc(var(--width-content-width) / 3 - var(--vw-1_5) / 3 * 2);
|
||||
--width-carousel-item-4: calc(var(--width-content-width) / 4 - var(--vw-1_5) / 4 * 3);
|
||||
|
||||
/* @typography/text-sizing/medium */
|
||||
--text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.72vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.82vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.92vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.1vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.3vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.6vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem);
|
||||
}
|
||||
|
||||
/* @typography/text-sizing/medium (mobile) */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--text-2xs: 2.5vw;
|
||||
--text-xs: 2.75vw;
|
||||
--text-sm: 3vw;
|
||||
--text-base: 3.25vw;
|
||||
--text-lg: 3.5vw;
|
||||
--text-xl: 4.25vw;
|
||||
--text-2xl: 5vw;
|
||||
--text-3xl: 6vw;
|
||||
--text-4xl: 7vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw;
|
||||
--width-content-width: 80vw;
|
||||
--width-carousel-padding: calc((100vw - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls: calc((100vw - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-item-2: var(--width-content-width);
|
||||
--width-carousel-item-3: var(--width-content-width);
|
||||
--width-carousel-item-4: var(--width-content-width);
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* Colors */
|
||||
--color-background: var(--background);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary-cta: var(--primary-cta);
|
||||
--color-primary-cta-text: var(--primary-cta-text);
|
||||
--color-secondary-cta: var(--secondary-cta);
|
||||
--color-secondary-cta-text: var(--secondary-cta-text);
|
||||
--color-accent: var(--accent);
|
||||
--color-background-accent: var(--background-accent);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: '${publicSans.variable}', sans-serif;
|
||||
--font-mono: monospace;
|
||||
|
||||
/* Border Radius */
|
||||
--radius: var(--radius);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
/* Width */
|
||||
--width-content-width: var(--width-content-width);
|
||||
--width-carousel-padding: var(--width-carousel-padding);
|
||||
--width-carousel-padding-controls: var(--width-carousel-padding-controls);
|
||||
--width-carousel-item-2: var(--width-carousel-item-2);
|
||||
--width-carousel-item-3: var(--width-carousel-item-3);
|
||||
--width-carousel-item-4: var(--width-carousel-item-4);
|
||||
|
||||
/* Typography */
|
||||
--text-2xs: var(--text-2xs);
|
||||
--text-xs: var(--text-xs);
|
||||
--text-sm: var(--text-sm);
|
||||
--text-base: var(--text-base);
|
||||
--text-lg: var(--text-lg);
|
||||
--text-xl: var(--text-xl);
|
||||
--text-2xl: var(--text-2xl);
|
||||
--text-3xl: var(--text-3xl);
|
||||
--text-4xl: var(--text-4xl);
|
||||
--text-5xl: var(--text-5xl);
|
||||
--text-6xl: var(--text-6xl);
|
||||
--text-7xl: var(--text-7xl);
|
||||
--text-8xl: var(--text-8xl);
|
||||
--text-9xl: var(--text-9xl);
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 1) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: '${publicSans.variable}', sans-serif;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: '${publicSans.variable}', sans-serif;
|
||||
}
|
||||
|
||||
/* WEBILD_CARD_STYLE */
|
||||
/* @cards/solid */
|
||||
.card {
|
||||
background: var(--color-card);
|
||||
}
|
||||
|
||||
/* WEBILD_PRIMARY_BUTTON */
|
||||
/* @primaryButtons/metallic */
|
||||
.primary-button {
|
||||
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%);
|
||||
box-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);
|
||||
}
|
||||
|
||||
/* WEBILD_SECONDARY_BUTTON */
|
||||
/* @secondaryButtons/solid */
|
||||
.secondary-button {
|
||||
background: var(--color-secondary-cta);
|
||||
}
|
||||
10
src/lib/utils.ts
Normal file
10
src/lib/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: (string | undefined | null | false)[]) {
|
||||
return inputs.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function cls(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
16
src/main.tsx
Normal file
16
src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { initVisualEdit } from '@/utils/visual-edit'
|
||||
|
||||
initVisualEdit()
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
158
src/styles/animations.css
Normal file
158
src/styles/animations.css
Normal file
@@ -0,0 +1,158 @@
|
||||
/* @utilities/animations */
|
||||
|
||||
@keyframes pulsate {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 var(--accent);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 20px 10px transparent;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInOpacity {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInTranslate {
|
||||
from {
|
||||
transform: translateY(0.75vh);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0vh);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora {
|
||||
from {
|
||||
background-position: 50% 50%, 50% 50%;
|
||||
}
|
||||
to {
|
||||
background-position: 350% 50%, 350% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-reverse {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical-reverse {
|
||||
from {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-horizontal {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-horizontal-reverse {
|
||||
from {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
from {
|
||||
transform: rotate(var(--initial-position, 0deg)) translateX(var(--translate-position, 120px))
|
||||
rotate(calc(-1 * var(--initial-position, 0deg)));
|
||||
}
|
||||
to {
|
||||
transform: rotate(calc(var(--initial-position, 0deg) + 360deg))
|
||||
translateX(var(--translate-position, 120px))
|
||||
rotate(calc(-1 * (var(--initial-position, 0deg) + 360deg)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes map-dot-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.4);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.4);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
|
||||
.animate-pulsate {
|
||||
animation: pulsate 1.5s infinite;
|
||||
}
|
||||
|
||||
.animation-container {
|
||||
animation: fadeInOpacity 0.8s ease-in-out forwards, fadeInTranslate 0.6s forwards;
|
||||
}
|
||||
|
||||
.animation-container-fade {
|
||||
animation: fadeInOpacity 0.8s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 15s linear infinite;
|
||||
}
|
||||
|
||||
.animate-spin-reverse {
|
||||
animation: spin-reverse 10s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee-vertical {
|
||||
animation: marquee-vertical 40s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee-vertical-reverse {
|
||||
animation: marquee-vertical-reverse 40s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee-horizontal {
|
||||
animation: marquee-horizontal 15s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee-horizontal-reverse {
|
||||
animation: marquee-horizontal-reverse 15s linear infinite;
|
||||
}
|
||||
61
src/styles/masks.css
Normal file
61
src/styles/masks.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/* @utilities/masks */
|
||||
|
||||
.mask-fade-x {
|
||||
mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-padding-x {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black var(--width-x-padding-mask-fade),
|
||||
black calc(100% - var(--width-x-padding-mask-fade)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-fade-bottom {
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-y {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black var(--vw-1_5),
|
||||
black calc(100% - var(--vw-1_5)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-fade-y-medium {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 20%,
|
||||
black 80%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-fade-bottom-large {
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 75%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-bottom-long {
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 5%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-top-long {
|
||||
mask-image: linear-gradient(to top, black 0%, black 5%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-xy {
|
||||
mask-image: linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%),
|
||||
linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||
mask-composite: intersect;
|
||||
}
|
||||
|
||||
.mask-fade-top-overlay {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 60%);
|
||||
}
|
||||
1371
src/utils/visual-edit-script.ts
Normal file
1371
src/utils/visual-edit-script.ts
Normal file
File diff suppressed because it is too large
Load Diff
21
src/utils/visual-edit.ts
Normal file
21
src/utils/visual-edit.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getVisualEditScript, getVisualEditScriptRaw } from './visual-edit-script'
|
||||
|
||||
export { getVisualEditScript }
|
||||
|
||||
export function initVisualEdit() {
|
||||
// Inside iframe we must actually run the editor code so it can
|
||||
// attach listeners and interact with the DOM.
|
||||
if (window.self !== window.top) {
|
||||
const script = document.createElement('script')
|
||||
script.id = 'webild-visual-edit'
|
||||
script.textContent = getVisualEditScriptRaw()
|
||||
document.head.appendChild(script)
|
||||
return
|
||||
}
|
||||
|
||||
// Expose helper for parent tools/dev.
|
||||
if (import.meta.env.DEV) {
|
||||
window.__getVisualEditScript = getVisualEditScript
|
||||
}
|
||||
}
|
||||
|
||||
12
src/vite-env.d.ts
vendored
Normal file
12
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
declare module "*.css";
|
||||
declare module "*.svg";
|
||||
declare module "*.png";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__getVisualEditScript?: () => string
|
||||
__webildEditorInitialized?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
23
theme-options/README.md
Normal file
23
theme-options/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Theme Options Reference
|
||||
|
||||
All ThemeProvider configuration options from webild-components-2.
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
theme-options/
|
||||
├── colorThemes.json (32 light + 27 dark themes)
|
||||
├── fontThemes.json (17 single fonts + 7 pairings)
|
||||
├── cards/
|
||||
│ └── card-styles.md (14 variants)
|
||||
├── buttons/
|
||||
│ ├── primary-button-styles.md (15 variants)
|
||||
│ └── secondary-button-styles.md (4 variants)
|
||||
├── typography/
|
||||
│ └── text-sizing.md (10 presets)
|
||||
└── layout/
|
||||
├── content-width.md (6 presets)
|
||||
└── border-radius.md (3 presets)
|
||||
```
|
||||
111
theme-options/buttons/primary-button-styles.md
Normal file
111
theme-options/buttons/primary-button-styles.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Primary Button Styles
|
||||
|
||||
15 primary button style variants available.
|
||||
|
||||
---
|
||||
|
||||
## gradient
|
||||
|
||||
background: linear-gradient(to bottom, color-mix(in srgb, var(--color-primary-cta) 75%, transparent), var(--color-primary-cta));
|
||||
box-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);
|
||||
box-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:
|
||||
radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),
|
||||
radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),
|
||||
var(--color-primary-cta);
|
||||
box-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));
|
||||
box-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||
|
||||
---
|
||||
|
||||
## double-inset
|
||||
|
||||
background: var(--color-primary-cta);
|
||||
box-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);
|
||||
box-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%);
|
||||
box-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);
|
||||
border: 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%);
|
||||
box-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%);
|
||||
box-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);
|
||||
box-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%);
|
||||
box-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);
|
||||
box-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%);
|
||||
box-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%);
|
||||
box-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);
|
||||
43
theme-options/buttons/secondary-button-styles.md
Normal file
43
theme-options/buttons/secondary-button-styles.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Secondary Button Styles
|
||||
|
||||
4 secondary button style variants available.
|
||||
|
||||
---
|
||||
|
||||
## glass
|
||||
|
||||
backdrop-filter: blur(8px);
|
||||
background: linear-gradient(to bottom right, color-mix(in srgb, var(--color-secondary-cta) 80%, transparent), var(--color-secondary-cta));
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
border: 1px solid var(--color-secondary-cta);
|
||||
|
||||
---
|
||||
|
||||
## solid
|
||||
|
||||
background: var(--color-secondary-cta);
|
||||
|
||||
---
|
||||
|
||||
## layered
|
||||
|
||||
background:
|
||||
linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),
|
||||
linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),
|
||||
linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),
|
||||
linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),
|
||||
linear-gradient(color-mix(in srgb, var(--color-secondary-cta) 60%, transparent), color-mix(in srgb, var(--color-secondary-cta) 60%, transparent)),
|
||||
var(--color-secondary-cta);
|
||||
box-shadow:
|
||||
2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||
border: 1px solid var(--color-secondary-cta);
|
||||
|
||||
---
|
||||
|
||||
## radial-glow
|
||||
|
||||
background:
|
||||
radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),
|
||||
radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),
|
||||
var(--color-secondary-cta);
|
||||
box-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||
121
theme-options/cards/card-styles.md
Normal file
121
theme-options/cards/card-styles.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Card Styles
|
||||
|
||||
14 card style variants available.
|
||||
|
||||
---
|
||||
|
||||
## solid
|
||||
|
||||
background: var(--color-card);
|
||||
|
||||
---
|
||||
|
||||
## outline
|
||||
|
||||
background: var(--color-card);
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 25%, transparent);
|
||||
|
||||
---
|
||||
|
||||
## gradient-mesh
|
||||
|
||||
background:
|
||||
radial-gradient(at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, color-mix(in srgb, var(--color-accent) 10%, transparent) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 100%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0px, transparent 50%),
|
||||
radial-gradient(at 0% 100%, color-mix(in srgb, var(--color-accent) 12%, transparent) 0px, transparent 50%),
|
||||
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%);
|
||||
box-shadow:
|
||||
inset 2px 2px 4px color-mix(in srgb, var(--color-foreground) 8%, transparent),
|
||||
inset -2px -2px 4px color-mix(in srgb, var(--color-background) 20%, transparent);
|
||||
|
||||
---
|
||||
|
||||
## glass-elevated
|
||||
|
||||
backdrop-filter: blur(8px);
|
||||
background: linear-gradient(to bottom right, color-mix(in srgb, var(--color-card) 80%, transparent), color-mix(in srgb, var(--color-card) 40%, transparent));
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
border: 1px solid var(--color-card);
|
||||
|
||||
---
|
||||
|
||||
## glass-depth
|
||||
|
||||
background: color-mix(in srgb, var(--color-card) 80%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow:
|
||||
inset 0 0 20px 0 color-mix(in srgb, var(--color-accent) 7.5%, transparent);
|
||||
border: 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%);
|
||||
box-shadow: 0px 0px 10px 4px color-mix(in srgb, var(--color-accent) 4%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 15%, transparent);
|
||||
|
||||
---
|
||||
|
||||
## layered-gradient
|
||||
|
||||
background:
|
||||
linear-gradient(color-mix(in srgb, var(--color-accent) 6%, transparent) 0%, transparent 59.26%),
|
||||
linear-gradient(var(--color-card) 0%, var(--color-card) 100%),
|
||||
var(--color-card);
|
||||
box-shadow:
|
||||
20px 18px 7px color-mix(in srgb, var(--color-accent) 0%, transparent),
|
||||
2px 2px 2px color-mix(in srgb, var(--color-accent) 6.5%, transparent),
|
||||
1px 1px 2px color-mix(in srgb, var(--color-accent) 2%, transparent);
|
||||
border: 2px solid var(--color-secondary-cta);
|
||||
|
||||
---
|
||||
|
||||
## soft-shadow
|
||||
|
||||
background: var(--color-card);
|
||||
box-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);
|
||||
box-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%);
|
||||
box-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);
|
||||
border: 1px solid color-mix(in srgb, var(--color-foreground) 6%, transparent);
|
||||
|
||||
---
|
||||
|
||||
## inner-glow
|
||||
|
||||
background: var(--color-card);
|
||||
box-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:
|
||||
radial-gradient(ellipse at 0% 0%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0%, transparent 50%),
|
||||
var(--color-card);
|
||||
box-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);
|
||||
677
theme-options/colorThemes.json
Normal file
677
theme-options/colorThemes.json
Normal file
@@ -0,0 +1,677 @@
|
||||
{
|
||||
"lightTheme": {
|
||||
"minimalDarkBlue": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000612e6",
|
||||
"--primary-cta": "#15479c",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000612e6"
|
||||
},
|
||||
"minimalDarkGreen": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000f06e6",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000f06e6"
|
||||
},
|
||||
"minimalLightRed": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120006e6",
|
||||
"--primary-cta": "#e63946",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120006e6"
|
||||
},
|
||||
"minimalBrightBlue": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000612e6",
|
||||
"--primary-cta": "#106EFB",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#106EFB",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#000612e6"
|
||||
},
|
||||
"minimalBrightOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#E34400",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#E34400",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"minimalGoldenOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#FF7B05",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#FF7B05",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#ff8c42",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#120a00e6"
|
||||
},
|
||||
"darkBlue": {
|
||||
"--background": "#f5faff",
|
||||
"--card": "#f1f8ff",
|
||||
"--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": "#f7fffa",
|
||||
"--foreground": "#001a0a",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#a8d9be",
|
||||
"--background-accent": "#6bbf8e",
|
||||
"--primary-cta-text": "#fafffb",
|
||||
"--secondary-cta-text": "#001a0a"
|
||||
},
|
||||
"lightRed": {
|
||||
"--background": "#fffafa",
|
||||
"--card": "#fff7f7",
|
||||
"--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": "#f7f5ff",
|
||||
"--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"
|
||||
},
|
||||
"warmgrayIndigo": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#0c1325",
|
||||
"--primary-cta": "#0b07ff",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#93b7ff",
|
||||
"--background-accent": "#a8bae8",
|
||||
"--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": "#fffefe",
|
||||
"--card": "#f6f7f4",
|
||||
"--foreground": "#080908",
|
||||
"--primary-cta": "#0e3a29",
|
||||
"--secondary-cta": "#e7eecd",
|
||||
"--accent": "#35c18b",
|
||||
"--background-accent": "#ecebe4",
|
||||
"--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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"lightBlueWhite": {
|
||||
"--background": "#010912",
|
||||
"--card": "#152840",
|
||||
"--foreground": "#e6f0ff",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#0e1a29",
|
||||
"--accent": "#3f5c79",
|
||||
"--background-accent": "#004a93",
|
||||
"--primary-cta-text": "#010912",
|
||||
"--secondary-cta-text": "#ffffff"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"crimson": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#f5f5f5",
|
||||
"--primary-cta": "#ff0000",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#991b1b",
|
||||
"--background-accent": "#7f1d1d",
|
||||
"--primary-cta-text": "#ffffff",
|
||||
"--secondary-cta-text": "#ffffff"
|
||||
},
|
||||
"midnightIce": {
|
||||
"--background": "#000000",
|
||||
"--card": "#0c0c0c",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#000000",
|
||||
"--accent": "#535353",
|
||||
"--background-accent": "#CEE7FF",
|
||||
"--primary-cta-text": "#000000",
|
||||
"--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"
|
||||
},
|
||||
"orangeBlueAccent": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#e34400",
|
||||
"--secondary-cta": "#010101",
|
||||
"--accent": "#ff7b05",
|
||||
"--background-accent": "#106efb",
|
||||
"--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"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffaf5e6",
|
||||
"--primary-cta": "#ffaa70",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373",
|
||||
"--primary-cta-text": "#0a0a0a",
|
||||
"--secondary-cta-text": "#fffaf5e6"
|
||||
},
|
||||
"minimalLightYellow": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffffae6",
|
||||
"--primary-cta": "#fde047",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373",
|
||||
"--primary-cta-text": "#0a0a0a",
|
||||
"--secondary-cta-text": "#fffffae6"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"indigo": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1f1f40",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d0d2b",
|
||||
"--accent": "#3d2880",
|
||||
"--background-accent": "#663cff",
|
||||
"--primary-cta-text": "#0a051a",
|
||||
"--secondary-cta-text": "#d4d4f6"
|
||||
},
|
||||
"forest": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1a2f1d",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d200f",
|
||||
"--accent": "#1a3d1f",
|
||||
"--background-accent": "#355e3b",
|
||||
"--primary-cta-text": "#0a1a0c",
|
||||
"--secondary-cta-text": "#d4f6d8"
|
||||
},
|
||||
"mint": {
|
||||
"--background": "#000000",
|
||||
"--card": "#1a2a1a",
|
||||
"--foreground": "#ffffff",
|
||||
"--primary-cta": "#ffffff",
|
||||
"--secondary-cta": "#0d1a0d",
|
||||
"--accent": "#2d4a2d",
|
||||
"--background-accent": "#c1e1c1",
|
||||
"--primary-cta-text": "#0a150a",
|
||||
"--secondary-cta-text": "#e1f6e1"
|
||||
}
|
||||
}
|
||||
}
|
||||
237
theme-options/fontThemes.json
Normal file
237
theme-options/fontThemes.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
theme-options/layout/border-radius.md
Normal file
21
theme-options/layout/border-radius.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Border Radius Presets
|
||||
|
||||
3 border radius presets available.
|
||||
|
||||
---
|
||||
|
||||
## rounded
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
---
|
||||
|
||||
## soft
|
||||
|
||||
--radius: 1rem;
|
||||
|
||||
---
|
||||
|
||||
## pill
|
||||
|
||||
--radius: 9999px;
|
||||
45
theme-options/layout/content-width.md
Normal file
45
theme-options/layout/content-width.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Content Width Presets
|
||||
|
||||
6 content width presets available.
|
||||
|
||||
---
|
||||
|
||||
## small
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 70vw, 100rem);
|
||||
Mobile: --width-content-width: 80vw;
|
||||
|
||||
---
|
||||
|
||||
## smallMedium
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 72.5vw, 100rem);
|
||||
Mobile: --width-content-width: 80vw;
|
||||
|
||||
---
|
||||
|
||||
## compact
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 75vw, 100rem);
|
||||
Mobile: --width-content-width: 80vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumSmall
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 77.5vw, 100rem);
|
||||
Mobile: --width-content-width: 80vw;
|
||||
|
||||
---
|
||||
|
||||
## medium
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 80vw, 100rem);
|
||||
Mobile: --width-content-width: 80vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumLarge
|
||||
|
||||
Desktop: --width-content-width: clamp(40rem, 82.5vw, 100rem);
|
||||
Mobile: --width-content-width: 85vw;
|
||||
375
theme-options/typography/text-sizing.md
Normal file
375
theme-options/typography/text-sizing.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Text Sizing Presets
|
||||
|
||||
10 text sizing presets available. Each preset defines 14 text sizes for desktop and mobile.
|
||||
|
||||
---
|
||||
|
||||
## medium
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.72vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.82vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.92vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.1vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.3vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.6vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.5vw;
|
||||
--text-xs: 2.75vw;
|
||||
--text-sm: 3vw;
|
||||
--text-base: 3.25vw;
|
||||
--text-lg: 3.5vw;
|
||||
--text-xl: 4.25vw;
|
||||
--text-2xl: 5vw;
|
||||
--text-3xl: 6vw;
|
||||
--text-4xl: 7vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumLarge
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.651vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.756vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.861vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.966vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.05vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.155vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.365vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.68vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.1vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.8875vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.465vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.2vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.725vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7.35vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.625vw;
|
||||
--text-xs: 2.8875vw;
|
||||
--text-sm: 3.15vw;
|
||||
--text-base: 3.4125vw;
|
||||
--text-lg: 3.675vw;
|
||||
--text-xl: 4.4625vw;
|
||||
--text-2xl: 5.25vw;
|
||||
--text-3xl: 6.3vw;
|
||||
--text-4xl: 7.35vw;
|
||||
--text-5xl: 7.875vw;
|
||||
--text-6xl: 8.925vw;
|
||||
--text-7xl: 10.5vw;
|
||||
--text-8xl: 12.6vw;
|
||||
--text-9xl: 14.7vw;
|
||||
|
||||
---
|
||||
|
||||
## largeSmall
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.682vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.792vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.902vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 1.012vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.21vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.43vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.76vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 3.025vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.63vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.95vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7.7vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.75vw;
|
||||
--text-xs: 3.025vw;
|
||||
--text-sm: 3.3vw;
|
||||
--text-base: 3.575vw;
|
||||
--text-lg: 3.85vw;
|
||||
--text-xl: 4.675vw;
|
||||
--text-2xl: 5.5vw;
|
||||
--text-3xl: 6.6vw;
|
||||
--text-4xl: 7.7vw;
|
||||
--text-5xl: 8.25vw;
|
||||
--text-6xl: 9.35vw;
|
||||
--text-7xl: 11vw;
|
||||
--text-8xl: 13.2vw;
|
||||
--text-9xl: 15.4vw;
|
||||
|
||||
---
|
||||
|
||||
## large
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.713vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.828vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.943vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 1.058vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.15vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.265vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.495vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.84vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.3vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 3.1625vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.795vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.6vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 5.175vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 8.05vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.875vw;
|
||||
--text-xs: 3.1625vw;
|
||||
--text-sm: 3.45vw;
|
||||
--text-base: 3.7375vw;
|
||||
--text-lg: 4.025vw;
|
||||
--text-xl: 4.8875vw;
|
||||
--text-2xl: 5.75vw;
|
||||
--text-3xl: 6.9vw;
|
||||
--text-4xl: 8.05vw;
|
||||
--text-5xl: 8.625vw;
|
||||
--text-6xl: 9.775vw;
|
||||
--text-7xl: 11.5vw;
|
||||
--text-8xl: 13.8vw;
|
||||
--text-9xl: 16.1vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumSizeLargeTitles
|
||||
|
||||
Medium body text (2xs-4xl) with large title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.72vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.82vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.92vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.1vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.3vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.6vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 3.1625vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.795vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.6vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 5.175vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 8.05vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.5vw;
|
||||
--text-xs: 2.75vw;
|
||||
--text-sm: 3vw;
|
||||
--text-base: 3.25vw;
|
||||
--text-lg: 3.5vw;
|
||||
--text-xl: 4.25vw;
|
||||
--text-2xl: 5vw;
|
||||
--text-3xl: 6vw;
|
||||
--text-4xl: 7vw;
|
||||
--text-5xl: 8.625vw;
|
||||
--text-6xl: 9.775vw;
|
||||
--text-7xl: 11.5vw;
|
||||
--text-8xl: 13.8vw;
|
||||
--text-9xl: 16.1vw;
|
||||
|
||||
---
|
||||
|
||||
## largeSizeMediumTitles
|
||||
|
||||
Large body text (2xs-4xl) with medium title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.713vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.828vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.943vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 1.058vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.15vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.265vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.495vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.84vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.3vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.875vw;
|
||||
--text-xs: 3.1625vw;
|
||||
--text-sm: 3.45vw;
|
||||
--text-base: 3.7375vw;
|
||||
--text-lg: 4.025vw;
|
||||
--text-xl: 4.8875vw;
|
||||
--text-2xl: 5.75vw;
|
||||
--text-3xl: 6.9vw;
|
||||
--text-4xl: 8.05vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumLargeSizeLargeTitles
|
||||
|
||||
MediumLarge body text (2xs-4xl) with large title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.651vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.756vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.861vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.966vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.05vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.155vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.365vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.68vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.1vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 3.1625vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.795vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.6vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 5.175vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 8.05vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.625vw;
|
||||
--text-xs: 2.8875vw;
|
||||
--text-sm: 3.15vw;
|
||||
--text-base: 3.4125vw;
|
||||
--text-lg: 3.675vw;
|
||||
--text-xl: 4.4625vw;
|
||||
--text-2xl: 5.25vw;
|
||||
--text-3xl: 6.3vw;
|
||||
--text-4xl: 7.35vw;
|
||||
--text-5xl: 8.625vw;
|
||||
--text-6xl: 9.775vw;
|
||||
--text-7xl: 11.5vw;
|
||||
--text-8xl: 13.8vw;
|
||||
--text-9xl: 16.1vw;
|
||||
|
||||
---
|
||||
|
||||
## largeSmallSizeLargeTitles
|
||||
|
||||
LargeSmall body text (2xs-4xl) with large title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.682vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.792vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.902vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 1.012vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.21vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.43vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.76vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 3.1625vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.795vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4.6vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 5.175vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 8.05vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.75vw;
|
||||
--text-xs: 3.025vw;
|
||||
--text-sm: 3.3vw;
|
||||
--text-base: 3.575vw;
|
||||
--text-lg: 3.85vw;
|
||||
--text-xl: 4.675vw;
|
||||
--text-2xl: 5.5vw;
|
||||
--text-3xl: 6.6vw;
|
||||
--text-4xl: 7.7vw;
|
||||
--text-5xl: 8.625vw;
|
||||
--text-6xl: 9.775vw;
|
||||
--text-7xl: 11.5vw;
|
||||
--text-8xl: 13.8vw;
|
||||
--text-9xl: 16.1vw;
|
||||
|
||||
---
|
||||
|
||||
## mediumLargeSizeMediumTitles
|
||||
|
||||
MediumLarge body text (2xs-4xl) with medium title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.651vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.756vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.861vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.966vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.05vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.155vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.365vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.68vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.1vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.625vw;
|
||||
--text-xs: 2.8875vw;
|
||||
--text-sm: 3.15vw;
|
||||
--text-base: 3.4125vw;
|
||||
--text-lg: 3.675vw;
|
||||
--text-xl: 4.4625vw;
|
||||
--text-2xl: 5.25vw;
|
||||
--text-3xl: 6.3vw;
|
||||
--text-4xl: 7.35vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw;
|
||||
|
||||
---
|
||||
|
||||
## largeSmallSizeMediumTitles
|
||||
|
||||
LargeSmall body text (2xs-4xl) with medium title sizes (5xl-9xl).
|
||||
|
||||
Desktop:
|
||||
--text-2xs: clamp(0.465rem, 0.682vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.792vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.902vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 1.012vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1.1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.21vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.43vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.76vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2.2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem);
|
||||
|
||||
Mobile:
|
||||
--text-2xs: 2.75vw;
|
||||
--text-xs: 3.025vw;
|
||||
--text-sm: 3.3vw;
|
||||
--text-base: 3.575vw;
|
||||
--text-lg: 3.85vw;
|
||||
--text-xl: 4.675vw;
|
||||
--text-2xl: 5.5vw;
|
||||
--text-3xl: 6.6vw;
|
||||
--text-4xl: 7.7vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw;
|
||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
vercel.json
Normal file
10
vercel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true,
|
||||
allowedHosts: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user