@siteable/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +4 -0
- package/README.md +82 -0
- package/dist/index.d.ts +401 -0
- package/dist/index.js +5389 -0
- package/package.json +66 -0
- package/src/blocks/BlockWrapper.tsx +149 -0
- package/src/blocks/banner/BannerBlock.tsx +38 -0
- package/src/blocks/contact/ContactBlock.tsx +63 -0
- package/src/blocks/content/ContentBlock.tsx +38 -0
- package/src/blocks/cta/CtaBlock.tsx +67 -0
- package/src/blocks/divider/DividerBlock.tsx +30 -0
- package/src/blocks/faq/FaqBlock.tsx +82 -0
- package/src/blocks/features/FeaturesBlock.tsx +170 -0
- package/src/blocks/footer/FooterBlock.tsx +136 -0
- package/src/blocks/gallery/GalleryBlock.tsx +67 -0
- package/src/blocks/hero/HeroBlock.tsx +163 -0
- package/src/blocks/image/ImageBlock.tsx +78 -0
- package/src/blocks/logocloud/LogoCloudBlock.tsx +70 -0
- package/src/blocks/navbar/NavbarBlock.tsx +102 -0
- package/src/blocks/newsletter/NewsletterBlock.tsx +51 -0
- package/src/blocks/pricing/PricingBlock.tsx +173 -0
- package/src/blocks/registry.tsx +90 -0
- package/src/blocks/stats/StatsBlock.tsx +96 -0
- package/src/blocks/team/TeamBlock.tsx +50 -0
- package/src/blocks/testimonials/TestimonialsBlock.tsx +203 -0
- package/src/blocks/types.ts +72 -0
- package/src/blocks/video/VideoBlock.tsx +58 -0
- package/src/editor/AgentPanel.tsx +318 -0
- package/src/editor/Canvas.tsx +95 -0
- package/src/editor/CanvasEmpty.tsx +40 -0
- package/src/editor/CanvasToolbar.tsx +366 -0
- package/src/editor/DesignPanel.tsx +224 -0
- package/src/editor/EditorLayout.tsx +196 -0
- package/src/editor/GenerationOverlay.tsx +201 -0
- package/src/editor/JsonDrawer.tsx +139 -0
- package/src/editor/LayersPanel.tsx +259 -0
- package/src/editor/LeftSidebar.tsx +136 -0
- package/src/editor/PropertiesPanel.tsx +564 -0
- package/src/editor/RightSidebar.tsx +85 -0
- package/src/editor/ShortcutsModal.tsx +126 -0
- package/src/editor/VersionHistory.tsx +110 -0
- package/src/index.ts +133 -0
- package/src/lib/block-metadata.ts +167 -0
- package/src/lib/export-html.ts +1424 -0
- package/src/lib/generate-site.ts +206 -0
- package/src/lib/generation-prompt.ts +64 -0
- package/src/lib/markdown.ts +88 -0
- package/src/lib/templates.ts +139 -0
- package/src/lib/theme-presets.ts +330 -0
- package/src/lib/useGoogleFonts.ts +49 -0
- package/src/lib/useScrollReveal.ts +28 -0
- package/src/settings/GeminiKeyInputField.tsx +82 -0
- package/src/store/configStore.ts +344 -0
- package/src/store/editorStore.ts +50 -0
- package/src/styles.css +210 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
import {
|
|
3
|
+
Blocks, Code, Bot, Zap, Shield, Globe,
|
|
4
|
+
Layers, Palette, Rocket, Star, Lock, Settings,
|
|
5
|
+
} from 'lucide-react'
|
|
6
|
+
import type { LucideIcon } from 'lucide-react'
|
|
7
|
+
|
|
8
|
+
interface FeatureItem {
|
|
9
|
+
icon?: string
|
|
10
|
+
title: string
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FeaturesProps {
|
|
15
|
+
label?: string
|
|
16
|
+
title: string
|
|
17
|
+
subtitle?: string
|
|
18
|
+
items: FeatureItem[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const iconMap: Record<string, LucideIcon> = {
|
|
22
|
+
Blocks, Code, Bot, Zap, Shield, Globe,
|
|
23
|
+
Layers, Palette, Rocket, Star, Lock, Settings,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getIcon(name?: string): LucideIcon {
|
|
27
|
+
if (!name) return Zap
|
|
28
|
+
return iconMap[name] || Zap
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function FeaturesGrid({ props }: { props: FeaturesProps }) {
|
|
32
|
+
return (
|
|
33
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
34
|
+
{/* Header */}
|
|
35
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-10">
|
|
36
|
+
{props.label && (
|
|
37
|
+
<div className="text-[11px] font-semibold uppercase tracking-widest text-green mb-2">
|
|
38
|
+
{props.label}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">{props.title}</h2>
|
|
42
|
+
{props.subtitle && (
|
|
43
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Grid */}
|
|
48
|
+
<div className="grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3 gap-4">
|
|
49
|
+
{props.items.map((item, i) => {
|
|
50
|
+
const Icon = getIcon(item.icon)
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
key={i}
|
|
54
|
+
className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} group bg-bg-2 border border-border-default rounded-xl p-5 transition-all hover:-translate-y-0.5 hover:border-border-hover hover:shadow-[0_8px_24px_rgba(0,0,0,0.2)]`}
|
|
55
|
+
>
|
|
56
|
+
<div className="w-10 h-10 rounded-lg bg-green/10 border border-green/20 flex items-center justify-center text-green mb-3 transition-all group-hover:bg-green/15 group-hover:accent-glow-md">
|
|
57
|
+
<Icon size={18} />
|
|
58
|
+
</div>
|
|
59
|
+
<h3 className="text-sm font-semibold mb-1">{item.title}</h3>
|
|
60
|
+
<p className="text-[12.5px] text-text-2 leading-relaxed">{item.description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function FeaturesList({ props }: { props: FeaturesProps }) {
|
|
70
|
+
return (
|
|
71
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
72
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-10">
|
|
73
|
+
{props.label && (
|
|
74
|
+
<div className="text-[11px] font-semibold uppercase tracking-widest text-green mb-2">
|
|
75
|
+
{props.label}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">{props.title}</h2>
|
|
79
|
+
{props.subtitle && (
|
|
80
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="max-w-2xl mx-auto space-y-4">
|
|
85
|
+
{props.items.map((item, i) => {
|
|
86
|
+
const Icon = getIcon(item.icon)
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
key={i}
|
|
90
|
+
className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} flex gap-4 p-4 rounded-xl bg-bg-2 border border-border-default transition-all hover:border-border-hover`}
|
|
91
|
+
>
|
|
92
|
+
<div className="w-10 h-10 rounded-lg bg-green/10 border border-green/20 flex items-center justify-center text-green shrink-0">
|
|
93
|
+
<Icon size={18} />
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<h3 className="text-sm font-semibold mb-0.5">{item.title}</h3>
|
|
97
|
+
<p className="text-[12.5px] text-text-2 leading-relaxed">{item.description}</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function FeaturesAlternating({ props }: { props: FeaturesProps }) {
|
|
108
|
+
return (
|
|
109
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
110
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-12">
|
|
111
|
+
{props.label && (
|
|
112
|
+
<div className="text-[11px] font-semibold uppercase tracking-widest text-green mb-2">
|
|
113
|
+
{props.label}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">{props.title}</h2>
|
|
117
|
+
{props.subtitle && (
|
|
118
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="space-y-8">
|
|
123
|
+
{props.items.map((item, i) => {
|
|
124
|
+
const Icon = getIcon(item.icon)
|
|
125
|
+
const imageUrl = (item as unknown as Record<string, unknown>).image as string | undefined
|
|
126
|
+
const isReversed = i % 2 === 1
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
key={i}
|
|
131
|
+
className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} flex flex-col @lg:flex-row items-center gap-6 @lg:gap-10 ${isReversed ? '@lg:flex-row-reverse' : ''}`}
|
|
132
|
+
>
|
|
133
|
+
{/* Image / placeholder */}
|
|
134
|
+
<div className="flex-1 w-full">
|
|
135
|
+
{imageUrl ? (
|
|
136
|
+
<img src={imageUrl} alt={item.title} className="w-full h-48 @lg:h-56 object-cover rounded-xl" />
|
|
137
|
+
) : (
|
|
138
|
+
<div className="w-full h-48 @lg:h-56 rounded-xl bg-bg-2 border border-border-default flex items-center justify-center">
|
|
139
|
+
<Icon size={32} className="text-green/30" />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
{/* Text */}
|
|
144
|
+
<div className="flex-1">
|
|
145
|
+
<div className="w-10 h-10 rounded-lg bg-green/10 border border-green/20 flex items-center justify-center text-green mb-3">
|
|
146
|
+
<Icon size={18} />
|
|
147
|
+
</div>
|
|
148
|
+
<h3 className="text-lg font-semibold mb-2">{item.title}</h3>
|
|
149
|
+
<p className="text-text-2 text-[13px] leading-relaxed">{item.description}</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function FeaturesBlock({ block }: { block: BlockConfig }) {
|
|
160
|
+
const props = block.props as unknown as FeaturesProps
|
|
161
|
+
|
|
162
|
+
switch (block.variant) {
|
|
163
|
+
case 'list':
|
|
164
|
+
return <FeaturesList props={props} />
|
|
165
|
+
case 'alternating':
|
|
166
|
+
return <FeaturesAlternating props={props} />
|
|
167
|
+
default:
|
|
168
|
+
return <FeaturesGrid props={props} />
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
interface FooterProps {
|
|
4
|
+
logo: string
|
|
5
|
+
logoImage?: string
|
|
6
|
+
copyright: string
|
|
7
|
+
links: string[]
|
|
8
|
+
columns?: { title: string; links: string[] }[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function FooterSimple({ props }: { props: FooterProps }) {
|
|
12
|
+
return (
|
|
13
|
+
<footer className="px-6 @md:px-10 py-8 border-t border-border-subtle">
|
|
14
|
+
<div className="flex flex-col @lg:flex-row items-center justify-between gap-4">
|
|
15
|
+
<div className="flex items-center gap-2">
|
|
16
|
+
{props.logoImage ? (
|
|
17
|
+
<img src={props.logoImage} alt={props.logo} className="h-6 w-auto object-contain" />
|
|
18
|
+
) : (
|
|
19
|
+
<div className="w-6 h-6 rounded-md bg-green/10 flex items-center justify-center">
|
|
20
|
+
<div className="w-3 h-3 rounded-full bg-green" />
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
<span className="text-sm font-semibold text-text-1">{props.logo}</span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="flex items-center gap-4">
|
|
27
|
+
{props.links.map((link, i) => (
|
|
28
|
+
<span
|
|
29
|
+
key={i}
|
|
30
|
+
className="text-[12px] text-text-3 hover:text-text-1 transition-colors cursor-pointer"
|
|
31
|
+
>
|
|
32
|
+
{link}
|
|
33
|
+
</span>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<span className="text-[11px] text-text-3">{props.copyright}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</footer>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function FooterMultiColumn({ props }: { props: FooterProps }) {
|
|
44
|
+
const columns = props.columns || [
|
|
45
|
+
{ title: 'Product', links: ['Features', 'Pricing', 'Changelog', 'Roadmap'] },
|
|
46
|
+
{ title: 'Company', links: ['About', 'Blog', 'Careers', 'Press'] },
|
|
47
|
+
{ title: 'Resources', links: ['Documentation', 'API Reference', 'Guides', 'Community'] },
|
|
48
|
+
{ title: 'Legal', links: ['Privacy', 'Terms', 'Security', 'Cookie Policy'] },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<footer className="px-6 @md:px-10 py-12 border-t border-border-subtle">
|
|
53
|
+
<div className="grid grid-cols-2 @2xl:grid-cols-5 gap-8 mb-10">
|
|
54
|
+
{/* Brand column */}
|
|
55
|
+
<div className="col-span-2 @2xl:col-span-1">
|
|
56
|
+
<div className="flex items-center gap-2 mb-3">
|
|
57
|
+
{props.logoImage ? (
|
|
58
|
+
<img src={props.logoImage} alt={props.logo} className="h-7 w-auto object-contain" />
|
|
59
|
+
) : (
|
|
60
|
+
<div className="w-7 h-7 rounded-md bg-green/10 flex items-center justify-center">
|
|
61
|
+
<div className="w-3.5 h-3.5 rounded-full bg-green" />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
<span className="text-sm font-semibold">{props.logo}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<p className="text-[12px] text-text-3 leading-relaxed max-w-[200px]">
|
|
67
|
+
Build beautiful websites with structured JSON config.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Link columns */}
|
|
72
|
+
{columns.map((col, i) => (
|
|
73
|
+
<div key={i}>
|
|
74
|
+
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-text-2 mb-3">
|
|
75
|
+
{col.title}
|
|
76
|
+
</h4>
|
|
77
|
+
<ul className="space-y-2">
|
|
78
|
+
{col.links.map((link, j) => (
|
|
79
|
+
<li key={j}>
|
|
80
|
+
<span className="text-[12.5px] text-text-3 hover:text-text-1 transition-colors cursor-pointer">
|
|
81
|
+
{link}
|
|
82
|
+
</span>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Bottom bar */}
|
|
91
|
+
<div className="pt-6 border-t border-border-subtle flex flex-col @lg:flex-row items-center justify-between gap-3">
|
|
92
|
+
<span className="text-[11px] text-text-3">{props.copyright}</span>
|
|
93
|
+
<div className="flex gap-4">
|
|
94
|
+
{props.links.map((link, i) => (
|
|
95
|
+
<span
|
|
96
|
+
key={i}
|
|
97
|
+
className="text-[11px] text-text-3 hover:text-text-1 transition-colors cursor-pointer"
|
|
98
|
+
>
|
|
99
|
+
{link}
|
|
100
|
+
</span>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</footer>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function FooterMinimal({ props }: { props: FooterProps }) {
|
|
109
|
+
return (
|
|
110
|
+
<footer className="px-6 @md:px-10 py-6">
|
|
111
|
+
<div className="flex items-center justify-center gap-1.5 text-[11px] text-text-3">
|
|
112
|
+
<span>{props.copyright}</span>
|
|
113
|
+
{props.links.length > 0 && <span className="mx-1">|</span>}
|
|
114
|
+
{props.links.map((link, i) => (
|
|
115
|
+
<span key={i}>
|
|
116
|
+
<span className="hover:text-text-1 transition-colors cursor-pointer">{link}</span>
|
|
117
|
+
{i < props.links.length - 1 && <span className="mx-1">|</span>}
|
|
118
|
+
</span>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</footer>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function FooterBlock({ block }: { block: BlockConfig }) {
|
|
126
|
+
const props = block.props as unknown as FooterProps
|
|
127
|
+
|
|
128
|
+
switch (block.variant) {
|
|
129
|
+
case 'multi-column':
|
|
130
|
+
return <FooterMultiColumn props={props} />
|
|
131
|
+
case 'minimal':
|
|
132
|
+
return <FooterMinimal props={props} />
|
|
133
|
+
default:
|
|
134
|
+
return <FooterSimple props={props} />
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ImageIcon } from 'lucide-react'
|
|
2
|
+
import type { BlockConfig } from '../types'
|
|
3
|
+
|
|
4
|
+
interface GalleryImage {
|
|
5
|
+
src?: string
|
|
6
|
+
alt?: string
|
|
7
|
+
caption?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const defaultImages: GalleryImage[] = [
|
|
11
|
+
{ alt: 'Image 1' }, { alt: 'Image 2' }, { alt: 'Image 3' },
|
|
12
|
+
{ alt: 'Image 4' }, { alt: 'Image 5' }, { alt: 'Image 6' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function ImageCard({ image, tall }: { image: GalleryImage; tall?: boolean }) {
|
|
16
|
+
return (
|
|
17
|
+
<div className={`rounded-lg overflow-hidden border border-border-default group ${tall ? 'row-span-2' : ''}`}>
|
|
18
|
+
{image.src ? (
|
|
19
|
+
<img src={image.src} alt={image.alt || ''} className="w-full h-full object-cover" />
|
|
20
|
+
) : (
|
|
21
|
+
<div className="w-full h-full min-h-[140px] bg-gradient-to-br from-bg-3 to-bg-4 flex items-center justify-center">
|
|
22
|
+
<ImageIcon size={24} className="text-text-3" />
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
{image.caption && (
|
|
26
|
+
<div className="px-3 py-2 bg-bg-2 text-[11px] text-text-2">{image.caption}</div>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function GalleryBlock({ block }: { block: BlockConfig }) {
|
|
33
|
+
const { variant, props } = block
|
|
34
|
+
const title = props.title as string | undefined
|
|
35
|
+
const images = ((props.images as GalleryImage[]) || []).length > 0
|
|
36
|
+
? (props.images as GalleryImage[])
|
|
37
|
+
: defaultImages
|
|
38
|
+
|
|
39
|
+
if (variant === 'masonry') {
|
|
40
|
+
return (
|
|
41
|
+
<div className="px-6 py-12 @lg:px-16 @lg:py-16">
|
|
42
|
+
{title && <h2 className="reveal-fade-up reveal-d1 text-2xl font-display font-semibold mb-6 text-center">{title}</h2>}
|
|
43
|
+
<div className="grid grid-cols-2 @lg:grid-cols-3 auto-rows-[160px] gap-3">
|
|
44
|
+
{images.map((img, i) => (
|
|
45
|
+
<div key={i} className={`reveal-scale reveal-d${Math.min(i + 2, 8)}`}>
|
|
46
|
+
<ImageCard image={img} tall={i % 3 === 0} />
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// grid (default)
|
|
55
|
+
return (
|
|
56
|
+
<div className="px-6 py-12 @lg:px-16 @lg:py-16">
|
|
57
|
+
{title && <h2 className="reveal-fade-up reveal-d1 text-2xl font-display font-semibold mb-6 text-center">{title}</h2>}
|
|
58
|
+
<div className="grid grid-cols-2 @lg:grid-cols-3 gap-3">
|
|
59
|
+
{images.map((img, i) => (
|
|
60
|
+
<div key={i} className={`reveal-scale reveal-d${Math.min(i + 2, 8)}`}>
|
|
61
|
+
<ImageCard image={img} />
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
import { ArrowRight, Sparkles } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface HeroProps {
|
|
5
|
+
badge?: string
|
|
6
|
+
headline: string
|
|
7
|
+
subheadline: string
|
|
8
|
+
primaryCta: string
|
|
9
|
+
secondaryCta?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function HeroCentered({ props }: { props: HeroProps }) {
|
|
13
|
+
return (
|
|
14
|
+
<section className="px-6 @md:px-10 py-20 @md:py-28 text-center">
|
|
15
|
+
{/* Badge */}
|
|
16
|
+
{props.badge && (
|
|
17
|
+
<div className="reveal-fade-up reveal-d1 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-green/10 border border-green/20 text-green text-[11px] font-medium mb-6">
|
|
18
|
+
<Sparkles size={12} />
|
|
19
|
+
{props.badge}
|
|
20
|
+
</div>
|
|
21
|
+
)}
|
|
22
|
+
|
|
23
|
+
{/* Headline */}
|
|
24
|
+
<h1 className="reveal-fade-up reveal-d2 text-4xl @md:text-5xl font-bold tracking-tight leading-[1.1] mb-4 max-w-3xl mx-auto">
|
|
25
|
+
{props.headline}
|
|
26
|
+
</h1>
|
|
27
|
+
|
|
28
|
+
{/* Subheadline */}
|
|
29
|
+
<p className="reveal-fade-up reveal-d3 text-text-2 text-base @md:text-lg leading-relaxed max-w-xl mx-auto mb-8">
|
|
30
|
+
{props.subheadline}
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
{/* CTAs */}
|
|
34
|
+
<div className="reveal-fade-up reveal-d4 flex flex-wrap items-center justify-center gap-3">
|
|
35
|
+
<button className="px-6 py-3 rounded-lg bg-green text-black text-sm font-semibold hover:bg-green-dim transition-all hover:accent-glow-xl flex items-center gap-2">
|
|
36
|
+
{props.primaryCta}
|
|
37
|
+
<ArrowRight size={16} />
|
|
38
|
+
</button>
|
|
39
|
+
{props.secondaryCta && (
|
|
40
|
+
<button className="px-6 py-3 rounded-lg bg-bg-3 text-text-0 text-sm font-medium border border-border-default hover:bg-bg-4 hover:border-border-hover transition-all">
|
|
41
|
+
{props.secondaryCta}
|
|
42
|
+
</button>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function HeroSplit({ props }: { props: HeroProps }) {
|
|
50
|
+
return (
|
|
51
|
+
<section className="px-6 @md:px-10 py-16 @md:py-24 flex flex-col @2xl:flex-row items-center gap-10">
|
|
52
|
+
{/* Text side */}
|
|
53
|
+
<div className="flex-1">
|
|
54
|
+
{props.badge && (
|
|
55
|
+
<div className="reveal-fade-up reveal-d1 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-green/10 border border-green/20 text-green text-[11px] font-medium mb-4">
|
|
56
|
+
<Sparkles size={12} />
|
|
57
|
+
{props.badge}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
<h1 className="reveal-fade-up reveal-d2 text-3xl @md:text-5xl font-bold tracking-tight leading-[1.1] mb-4">
|
|
61
|
+
{props.headline}
|
|
62
|
+
</h1>
|
|
63
|
+
<p className="reveal-fade-up reveal-d3 text-text-2 text-base leading-relaxed mb-6 max-w-lg">
|
|
64
|
+
{props.subheadline}
|
|
65
|
+
</p>
|
|
66
|
+
<div className="reveal-fade-up reveal-d4 flex flex-wrap items-center gap-3">
|
|
67
|
+
<button className="px-6 py-3 rounded-lg bg-green text-black text-sm font-semibold hover:bg-green-dim transition-all flex items-center gap-2">
|
|
68
|
+
{props.primaryCta}
|
|
69
|
+
<ArrowRight size={16} />
|
|
70
|
+
</button>
|
|
71
|
+
{props.secondaryCta && (
|
|
72
|
+
<button className="px-6 py-3 rounded-lg bg-bg-3 text-text-0 text-sm font-medium border border-border-default hover:bg-bg-4 transition-all">
|
|
73
|
+
{props.secondaryCta}
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Visual side */}
|
|
80
|
+
<div className="reveal-fade-up reveal-d3 flex-1 w-full">
|
|
81
|
+
<div className="aspect-[4/3] rounded-xl bg-bg-2 border border-border-default overflow-hidden relative">
|
|
82
|
+
<div className="absolute inset-0 bg-gradient-to-br from-green/5 to-transparent" />
|
|
83
|
+
<div className="absolute inset-6 border border-dashed border-border-default rounded-lg flex items-center justify-center text-text-3 text-sm">
|
|
84
|
+
Preview
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function HeroGradient({ props }: { props: HeroProps }) {
|
|
93
|
+
return (
|
|
94
|
+
<section className="px-6 @md:px-10 py-20 @md:py-32 text-center relative overflow-hidden">
|
|
95
|
+
{/* Gradient background */}
|
|
96
|
+
<div className="absolute inset-0 bg-gradient-to-b from-green/5 via-transparent to-transparent pointer-events-none" />
|
|
97
|
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-green/8 rounded-full blur-[100px] pointer-events-none" />
|
|
98
|
+
|
|
99
|
+
<div className="relative z-10">
|
|
100
|
+
{props.badge && (
|
|
101
|
+
<div className="reveal-fade-up reveal-d1 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-green/10 border border-green/20 text-green text-[11px] font-medium mb-6">
|
|
102
|
+
<Sparkles size={12} />
|
|
103
|
+
{props.badge}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
<h1 className="reveal-fade-up reveal-d2 text-4xl @md:text-5xl font-bold tracking-tight leading-[1.1] mb-4 max-w-3xl mx-auto" style={{ color: 'var(--color-text-0)' }}>
|
|
108
|
+
{props.headline}
|
|
109
|
+
</h1>
|
|
110
|
+
|
|
111
|
+
<p className="reveal-fade-up reveal-d3 text-text-2 text-base @md:text-lg leading-relaxed max-w-xl mx-auto mb-8">
|
|
112
|
+
{props.subheadline}
|
|
113
|
+
</p>
|
|
114
|
+
|
|
115
|
+
<div className="reveal-fade-up reveal-d4 flex flex-wrap items-center justify-center gap-3">
|
|
116
|
+
<button className="px-6 py-3 rounded-lg bg-green text-black text-sm font-semibold hover:bg-green-dim transition-all hover:accent-glow-xl flex items-center gap-2">
|
|
117
|
+
{props.primaryCta}
|
|
118
|
+
<ArrowRight size={16} />
|
|
119
|
+
</button>
|
|
120
|
+
{props.secondaryCta && (
|
|
121
|
+
<button className="px-6 py-3 rounded-lg bg-bg-3 text-text-0 text-sm font-medium border border-border-default hover:bg-bg-4 transition-all">
|
|
122
|
+
{props.secondaryCta}
|
|
123
|
+
</button>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function HeroMinimal({ props }: { props: HeroProps }) {
|
|
132
|
+
return (
|
|
133
|
+
<section className="px-6 @md:px-10 py-24 @md:py-36 text-center">
|
|
134
|
+
<h1 className="reveal-fade-up reveal-d1 text-5xl @md:text-7xl font-bold tracking-tighter leading-[1.05] mb-6 max-w-4xl mx-auto">
|
|
135
|
+
{props.headline}
|
|
136
|
+
</h1>
|
|
137
|
+
<p className="reveal-fade-up reveal-d2 text-text-2 text-lg @md:text-xl leading-relaxed max-w-lg mx-auto mb-10">
|
|
138
|
+
{props.subheadline}
|
|
139
|
+
</p>
|
|
140
|
+
<div className="reveal-fade-up reveal-d3">
|
|
141
|
+
<button className="px-8 py-4 rounded-lg bg-green text-black text-sm font-semibold hover:bg-green-dim transition-all hover:accent-glow-xl flex items-center gap-2 mx-auto">
|
|
142
|
+
{props.primaryCta}
|
|
143
|
+
<ArrowRight size={16} />
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function HeroBlock({ block }: { block: BlockConfig }) {
|
|
151
|
+
const props = block.props as unknown as HeroProps
|
|
152
|
+
|
|
153
|
+
switch (block.variant) {
|
|
154
|
+
case 'split':
|
|
155
|
+
return <HeroSplit props={props} />
|
|
156
|
+
case 'gradient':
|
|
157
|
+
return <HeroGradient props={props} />
|
|
158
|
+
case 'minimal':
|
|
159
|
+
return <HeroMinimal props={props} />
|
|
160
|
+
default:
|
|
161
|
+
return <HeroCentered props={props} />
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ImageIcon } from 'lucide-react'
|
|
2
|
+
import type { BlockConfig } from '../types'
|
|
3
|
+
|
|
4
|
+
function Placeholder({ className }: { className?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className={`bg-gradient-to-br from-bg-3 to-bg-4 flex items-center justify-center ${className || ''}`}>
|
|
7
|
+
<ImageIcon size={32} className="text-text-3" />
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ImageBlock({ block }: { block: BlockConfig }) {
|
|
13
|
+
const { variant, props } = block
|
|
14
|
+
const src = props.src as string | undefined
|
|
15
|
+
const alt = (props.alt as string) || ''
|
|
16
|
+
const title = props.title as string | undefined
|
|
17
|
+
const subtitle = props.subtitle as string | undefined
|
|
18
|
+
const imageSide = (props.imageSide as string) || 'left'
|
|
19
|
+
const images = (props.images as { src?: string; alt?: string }[]) || []
|
|
20
|
+
|
|
21
|
+
if (variant === 'side-by-side') {
|
|
22
|
+
const imgEl = src
|
|
23
|
+
? <img src={src} alt={alt} className="w-full h-full object-cover rounded-lg" />
|
|
24
|
+
: <Placeholder className="w-full h-64 @lg:h-80 rounded-lg" />
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="px-6 py-12 @lg:px-16 @lg:py-16">
|
|
28
|
+
<div className={`grid grid-cols-1 @lg:grid-cols-2 gap-8 @lg:gap-12 items-center ${imageSide === 'right' ? '' : ''}`}>
|
|
29
|
+
<div className={`reveal-scale reveal-d1 ${imageSide === 'right' ? '@lg:order-2' : ''}`}>
|
|
30
|
+
{imgEl}
|
|
31
|
+
</div>
|
|
32
|
+
<div className={`reveal-fade-up reveal-d2 ${imageSide === 'right' ? '@lg:order-1' : ''}`}>
|
|
33
|
+
{title && <h2 className="text-2xl font-display font-semibold mb-3">{title}</h2>}
|
|
34
|
+
{subtitle && <p className="text-text-1 leading-relaxed">{subtitle}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (variant === 'grid') {
|
|
42
|
+
const gridImages = images.length > 0
|
|
43
|
+
? images.slice(0, 4)
|
|
44
|
+
: [{ src: undefined, alt: '1' }, { src: undefined, alt: '2' }, { src: undefined, alt: '3' }, { src: undefined, alt: '4' }]
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="px-6 py-12 @lg:px-16 @lg:py-16">
|
|
48
|
+
{title && <h2 className="reveal-fade-up reveal-d1 text-2xl font-display font-semibold mb-6 text-center">{title}</h2>}
|
|
49
|
+
<div className="grid grid-cols-2 @lg:grid-cols-4 gap-3">
|
|
50
|
+
{gridImages.map((img, i) => (
|
|
51
|
+
<div key={i} className={`reveal-scale reveal-d${Math.min(i + 2, 8)} aspect-square rounded-lg overflow-hidden`}>
|
|
52
|
+
{img.src
|
|
53
|
+
? <img src={img.src} alt={img.alt || ''} className="w-full h-full object-cover" />
|
|
54
|
+
: <Placeholder className="w-full h-full" />
|
|
55
|
+
}
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// hero-image (default)
|
|
64
|
+
return (
|
|
65
|
+
<div className="reveal-scale reveal-d1 relative">
|
|
66
|
+
{src
|
|
67
|
+
? <img src={src} alt={alt} className="w-full h-64 @lg:h-96 object-cover" />
|
|
68
|
+
: <Placeholder className="w-full h-64 @lg:h-96" />
|
|
69
|
+
}
|
|
70
|
+
{(title || subtitle) && (
|
|
71
|
+
<div className="absolute inset-0 bg-gradient-to-t from-bg-0/80 to-transparent flex flex-col justify-end p-6 @lg:p-12">
|
|
72
|
+
{title && <h2 className="reveal-fade-up reveal-d2 text-2xl @lg:text-3xl font-display font-bold mb-2">{title}</h2>}
|
|
73
|
+
{subtitle && <p className="reveal-fade-up reveal-d3 text-text-1 text-sm @lg:text-base max-w-lg">{subtitle}</p>}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
interface LogoCloudProps {
|
|
4
|
+
title?: string
|
|
5
|
+
logos?: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaultLogos = ['Vercel', 'Stripe', 'GitHub', 'Figma', 'Notion', 'Linear']
|
|
9
|
+
|
|
10
|
+
function LogoPlaceholder({ name }: { name: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="group cursor-pointer transition-all">
|
|
13
|
+
<div className="flex items-center gap-2 opacity-50 group-hover:opacity-100 transition-all group-hover:scale-105 grayscale group-hover:grayscale-0">
|
|
14
|
+
<svg
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
width="28"
|
|
17
|
+
height="28"
|
|
18
|
+
viewBox="0 0 28 28"
|
|
19
|
+
className="text-text-3 group-hover:text-green transition-colors"
|
|
20
|
+
>
|
|
21
|
+
<rect
|
|
22
|
+
x="1"
|
|
23
|
+
y="1"
|
|
24
|
+
width="26"
|
|
25
|
+
height="26"
|
|
26
|
+
rx="6"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="1.5"
|
|
30
|
+
/>
|
|
31
|
+
<text
|
|
32
|
+
x="14"
|
|
33
|
+
y="18"
|
|
34
|
+
textAnchor="middle"
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
fontSize="14"
|
|
37
|
+
fontWeight="700"
|
|
38
|
+
fontFamily="system-ui, sans-serif"
|
|
39
|
+
>
|
|
40
|
+
{name.charAt(0).toUpperCase()}
|
|
41
|
+
</text>
|
|
42
|
+
</svg>
|
|
43
|
+
<span className="text-sm font-semibold text-text-3 group-hover:text-text-0 tracking-tight transition-colors">
|
|
44
|
+
{name}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function LogoCloudBlock({ block }: { block: BlockConfig }) {
|
|
52
|
+
const props = block.props as unknown as LogoCloudProps
|
|
53
|
+
const logos = props.logos || defaultLogos
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className="px-6 @md:px-10 py-10 @md:py-14">
|
|
57
|
+
<p className="reveal-fade-up reveal-d1 text-center text-[11px] font-medium uppercase tracking-widest text-text-3 mb-6">
|
|
58
|
+
{props.title || 'Trusted by teams at'}
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<div className="flex flex-wrap items-center justify-center gap-8 @md:gap-12">
|
|
62
|
+
{logos.map((logo, i) => (
|
|
63
|
+
<div key={i} className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)}`}>
|
|
64
|
+
<LogoPlaceholder name={logo} />
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
)
|
|
70
|
+
}
|