@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,102 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
import { Menu } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface NavbarProps {
|
|
5
|
+
logo: string
|
|
6
|
+
links: string[]
|
|
7
|
+
ctaText: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function NavbarDefault({ props }: { props: NavbarProps }) {
|
|
11
|
+
const { logo, links = [], ctaText } = props
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<nav className="px-6 @md:px-10 py-4 flex items-center justify-between">
|
|
15
|
+
{/* Logo */}
|
|
16
|
+
<div className="flex items-center gap-2">
|
|
17
|
+
<div className="w-8 h-8 rounded-lg bg-green/10 flex items-center justify-center">
|
|
18
|
+
<div className="w-4 h-4 rounded-full bg-green" />
|
|
19
|
+
</div>
|
|
20
|
+
<span className="font-semibold text-[15px] text-text-0 tracking-tight">{logo}</span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
{/* Desktop nav links */}
|
|
24
|
+
<div className="hidden @2xl:flex items-center gap-6">
|
|
25
|
+
{links.map((link, i) => (
|
|
26
|
+
<span
|
|
27
|
+
key={i}
|
|
28
|
+
className="text-[13px] text-text-2 hover:text-text-0 transition-colors cursor-pointer"
|
|
29
|
+
>
|
|
30
|
+
{link}
|
|
31
|
+
</span>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* CTA + mobile menu */}
|
|
36
|
+
<div className="flex items-center gap-3">
|
|
37
|
+
<button className="px-4 py-2 rounded-lg bg-green text-black text-[13px] font-semibold hover:bg-green-dim transition-colors">
|
|
38
|
+
{ctaText}
|
|
39
|
+
</button>
|
|
40
|
+
<button className="@2xl:hidden w-9 h-9 rounded-lg border border-border-default flex items-center justify-center text-text-2 hover:text-text-0 hover:bg-bg-3 transition-colors">
|
|
41
|
+
<Menu size={16} />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</nav>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function NavbarCentered({ props }: { props: NavbarProps }) {
|
|
49
|
+
const { logo, links = [], ctaText } = props
|
|
50
|
+
const mid = Math.ceil(links.length / 2)
|
|
51
|
+
const leftLinks = links.slice(0, mid)
|
|
52
|
+
const rightLinks = links.slice(mid)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<nav className="px-6 @md:px-10 py-4 flex items-center justify-between">
|
|
56
|
+
{/* Left links */}
|
|
57
|
+
<div className="hidden @2xl:flex items-center gap-6 flex-1">
|
|
58
|
+
{leftLinks.map((link, i) => (
|
|
59
|
+
<span key={i} className="text-[13px] text-text-2 hover:text-text-0 transition-colors cursor-pointer">
|
|
60
|
+
{link}
|
|
61
|
+
</span>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Center logo */}
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<div className="w-8 h-8 rounded-lg bg-green/10 flex items-center justify-center">
|
|
68
|
+
<div className="w-4 h-4 rounded-full bg-green" />
|
|
69
|
+
</div>
|
|
70
|
+
<span className="font-semibold text-[15px] text-text-0 tracking-tight">{logo}</span>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Right links + CTA */}
|
|
74
|
+
<div className="hidden @2xl:flex items-center gap-6 flex-1 justify-end">
|
|
75
|
+
{rightLinks.map((link, i) => (
|
|
76
|
+
<span key={i} className="text-[13px] text-text-2 hover:text-text-0 transition-colors cursor-pointer">
|
|
77
|
+
{link}
|
|
78
|
+
</span>
|
|
79
|
+
))}
|
|
80
|
+
<button className="px-4 py-2 rounded-lg bg-green text-black text-[13px] font-semibold hover:bg-green-dim transition-colors ml-2">
|
|
81
|
+
{ctaText}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Mobile menu */}
|
|
86
|
+
<button className="@2xl:hidden w-9 h-9 rounded-lg border border-border-default flex items-center justify-center text-text-2 hover:text-text-0 hover:bg-bg-3 transition-colors">
|
|
87
|
+
<Menu size={16} />
|
|
88
|
+
</button>
|
|
89
|
+
</nav>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function NavbarBlock({ block }: { block: BlockConfig }) {
|
|
94
|
+
const props = block.props as unknown as NavbarProps
|
|
95
|
+
|
|
96
|
+
switch (block.variant) {
|
|
97
|
+
case 'centered':
|
|
98
|
+
return <NavbarCentered props={props} />
|
|
99
|
+
default:
|
|
100
|
+
return <NavbarDefault props={props} />
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { toast } from 'sonner'
|
|
2
|
+
import type { BlockConfig } from '../types'
|
|
3
|
+
import { Mail } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
interface NewsletterProps {
|
|
6
|
+
title?: string
|
|
7
|
+
subtitle?: string
|
|
8
|
+
buttonText?: string
|
|
9
|
+
socialProof?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NewsletterBlock({ block }: { block: BlockConfig }) {
|
|
13
|
+
const props = block.props as unknown as NewsletterProps
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<section className="px-6 @md:px-10 py-12 @md:py-16">
|
|
17
|
+
<div className="max-w-xl mx-auto text-center">
|
|
18
|
+
<div className="reveal-scale reveal-d1 w-12 h-12 rounded-xl bg-green/10 border border-green/20 flex items-center justify-center text-green mx-auto mb-4">
|
|
19
|
+
<Mail size={22} />
|
|
20
|
+
</div>
|
|
21
|
+
<h2 className="reveal-fade-up reveal-d2 text-xl @md:text-2xl font-bold tracking-tight mb-2">
|
|
22
|
+
{props.title || 'Stay in the loop'}
|
|
23
|
+
</h2>
|
|
24
|
+
<p className="reveal-fade-up reveal-d3 text-text-2 text-sm mb-6">
|
|
25
|
+
{props.subtitle || 'Get updates on new features and releases. No spam, ever.'}
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<div className="reveal-fade-up reveal-d4 flex gap-2 max-w-sm mx-auto">
|
|
29
|
+
<input
|
|
30
|
+
type="email"
|
|
31
|
+
placeholder="you@example.com"
|
|
32
|
+
className="flex-1 px-4 py-2.5 rounded-lg border border-border-default bg-bg-2 text-text-0 text-[13px] outline-none focus:border-green placeholder:text-text-3 transition-colors"
|
|
33
|
+
/>
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => toast("You're subscribed!")}
|
|
36
|
+
className="px-5 py-2.5 rounded-lg bg-green text-black text-sm font-semibold hover:bg-green-dim transition-all shrink-0"
|
|
37
|
+
>
|
|
38
|
+
{props.buttonText || 'Subscribe'}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{props.socialProof && (
|
|
43
|
+
<p className="text-[11px] text-text-3 mt-3">{props.socialProof}</p>
|
|
44
|
+
)}
|
|
45
|
+
{!props.socialProof && (
|
|
46
|
+
<p className="text-[11px] text-text-3 mt-3">Join 2,000+ developers and designers</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
import { Check, Star } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface PricingTier {
|
|
5
|
+
name: string
|
|
6
|
+
price: string
|
|
7
|
+
period?: string
|
|
8
|
+
description?: string
|
|
9
|
+
features: string[]
|
|
10
|
+
cta: string
|
|
11
|
+
featured?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PricingProps {
|
|
15
|
+
title: string
|
|
16
|
+
subtitle?: string
|
|
17
|
+
tiers?: PricingTier[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultTiers: PricingTier[] = [
|
|
21
|
+
{
|
|
22
|
+
name: 'Starter',
|
|
23
|
+
price: '$0',
|
|
24
|
+
period: '/month',
|
|
25
|
+
description: 'For personal projects',
|
|
26
|
+
features: ['1 website', '5 blocks', 'Basic export', 'Community support'],
|
|
27
|
+
cta: 'Get Started',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Pro',
|
|
31
|
+
price: '$19',
|
|
32
|
+
period: '/month',
|
|
33
|
+
description: 'For professionals',
|
|
34
|
+
features: ['Unlimited websites', 'All blocks', 'Custom domains', 'Priority support', 'Agent API access', 'Version history'],
|
|
35
|
+
cta: 'Upgrade to Pro',
|
|
36
|
+
featured: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Team',
|
|
40
|
+
price: '$49',
|
|
41
|
+
period: '/month',
|
|
42
|
+
description: 'For teams and agencies',
|
|
43
|
+
features: ['Everything in Pro', 'Team collaboration', 'Custom components', 'SSO', 'Dedicated support'],
|
|
44
|
+
cta: 'Contact Sales',
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
function PricingSimple({ props }: { props: PricingProps }) {
|
|
49
|
+
const tiers = props.tiers || defaultTiers
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
53
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-10">
|
|
54
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">{props.title}</h2>
|
|
55
|
+
{props.subtitle && (
|
|
56
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="grid grid-cols-1 @2xl:grid-cols-3 gap-4 max-w-4xl mx-auto">
|
|
61
|
+
{tiers.map((tier, i) => (
|
|
62
|
+
<div
|
|
63
|
+
key={i}
|
|
64
|
+
className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} relative rounded-xl p-6 flex flex-col transition-all ${
|
|
65
|
+
tier.featured
|
|
66
|
+
? 'bg-bg-2 border-2 border-green accent-glow-ring'
|
|
67
|
+
: 'bg-bg-2 border border-border-default hover:border-border-hover'
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
{tier.featured && (
|
|
71
|
+
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-0.5 rounded-full bg-green text-black text-[10px] font-bold uppercase tracking-wider flex items-center gap-1">
|
|
72
|
+
<Star size={10} fill="currentColor" />
|
|
73
|
+
Recommended
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div className="mb-4">
|
|
78
|
+
<h3 className="text-sm font-semibold mb-1">{tier.name}</h3>
|
|
79
|
+
{tier.description && (
|
|
80
|
+
<p className="text-[11px] text-text-3">{tier.description}</p>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="mb-4">
|
|
85
|
+
<span className="text-3xl font-bold tracking-tight">{tier.price}</span>
|
|
86
|
+
{tier.period && (
|
|
87
|
+
<span className="text-text-3 text-sm">{tier.period}</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<ul className="space-y-2 mb-6 flex-1">
|
|
92
|
+
{tier.features.map((feature, j) => (
|
|
93
|
+
<li key={j} className="flex items-start gap-2 text-[12.5px] text-text-1">
|
|
94
|
+
<Check size={14} className="text-green shrink-0 mt-0.5" />
|
|
95
|
+
{feature}
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
className={`w-full py-2.5 rounded-lg text-sm font-semibold transition-all ${
|
|
102
|
+
tier.featured
|
|
103
|
+
? 'bg-green text-black hover:bg-green-dim hover:accent-glow-lg'
|
|
104
|
+
: 'bg-bg-3 text-text-0 border border-border-default hover:bg-bg-4 hover:border-border-hover'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{tier.cta}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function PricingComparison({ props }: { props: PricingProps }) {
|
|
117
|
+
const tiers = props.tiers || defaultTiers
|
|
118
|
+
const allFeatures = [...new Set(tiers.flatMap((t) => t.features))]
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
122
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-10">
|
|
123
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">{props.title}</h2>
|
|
124
|
+
{props.subtitle && (
|
|
125
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="reveal-fade-up reveal-d2 max-w-3xl mx-auto overflow-x-auto">
|
|
130
|
+
<table className="w-full text-left text-[12.5px]">
|
|
131
|
+
<thead>
|
|
132
|
+
<tr className="border-b border-border-default">
|
|
133
|
+
<th className="py-3 px-3 text-text-3 font-medium">Feature</th>
|
|
134
|
+
{tiers.map((tier, i) => (
|
|
135
|
+
<th key={i} className={`py-3 px-3 text-center font-semibold ${tier.featured ? 'text-green' : 'text-text-0'}`}>
|
|
136
|
+
{tier.name}
|
|
137
|
+
<div className="text-lg font-bold mt-0.5">{tier.price}</div>
|
|
138
|
+
</th>
|
|
139
|
+
))}
|
|
140
|
+
</tr>
|
|
141
|
+
</thead>
|
|
142
|
+
<tbody>
|
|
143
|
+
{allFeatures.map((feature, i) => (
|
|
144
|
+
<tr key={i} className="border-b border-border-subtle">
|
|
145
|
+
<td className="py-2.5 px-3 text-text-1">{feature}</td>
|
|
146
|
+
{tiers.map((tier, j) => (
|
|
147
|
+
<td key={j} className="py-2.5 px-3 text-center">
|
|
148
|
+
{tier.features.includes(feature) ? (
|
|
149
|
+
<Check size={14} className="text-green mx-auto" />
|
|
150
|
+
) : (
|
|
151
|
+
<span className="text-text-3">-</span>
|
|
152
|
+
)}
|
|
153
|
+
</td>
|
|
154
|
+
))}
|
|
155
|
+
</tr>
|
|
156
|
+
))}
|
|
157
|
+
</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function PricingBlock({ block }: { block: BlockConfig }) {
|
|
165
|
+
const props = block.props as unknown as PricingProps
|
|
166
|
+
|
|
167
|
+
switch (block.variant) {
|
|
168
|
+
case 'comparison':
|
|
169
|
+
return <PricingComparison props={props} />
|
|
170
|
+
default:
|
|
171
|
+
return <PricingSimple props={props} />
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BlockConfig } from './types'
|
|
2
|
+
import { Component, type ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { NavbarBlock } from './navbar/NavbarBlock'
|
|
5
|
+
import { HeroBlock } from './hero/HeroBlock'
|
|
6
|
+
import { FeaturesBlock } from './features/FeaturesBlock'
|
|
7
|
+
import { PricingBlock } from './pricing/PricingBlock'
|
|
8
|
+
import { CtaBlock } from './cta/CtaBlock'
|
|
9
|
+
import { FooterBlock } from './footer/FooterBlock'
|
|
10
|
+
import { TestimonialsBlock } from './testimonials/TestimonialsBlock'
|
|
11
|
+
import { StatsBlock } from './stats/StatsBlock'
|
|
12
|
+
import { FaqBlock } from './faq/FaqBlock'
|
|
13
|
+
import { TeamBlock } from './team/TeamBlock'
|
|
14
|
+
import { ContactBlock } from './contact/ContactBlock'
|
|
15
|
+
import { NewsletterBlock } from './newsletter/NewsletterBlock'
|
|
16
|
+
import { LogoCloudBlock } from './logocloud/LogoCloudBlock'
|
|
17
|
+
import { DividerBlock } from './divider/DividerBlock'
|
|
18
|
+
import { BannerBlock } from './banner/BannerBlock'
|
|
19
|
+
import { ContentBlock } from './content/ContentBlock'
|
|
20
|
+
import { ImageBlock } from './image/ImageBlock'
|
|
21
|
+
import { VideoBlock } from './video/VideoBlock'
|
|
22
|
+
import { GalleryBlock } from './gallery/GalleryBlock'
|
|
23
|
+
|
|
24
|
+
// Error boundary for individual blocks
|
|
25
|
+
class BlockErrorBoundary extends Component<
|
|
26
|
+
{ blockType: string; children: ReactNode },
|
|
27
|
+
{ hasError: boolean; error?: Error }
|
|
28
|
+
> {
|
|
29
|
+
state = { hasError: false, error: undefined as Error | undefined }
|
|
30
|
+
|
|
31
|
+
static getDerivedStateFromError(error: Error) {
|
|
32
|
+
return { hasError: true, error }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
if (this.state.hasError) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="px-6 py-8 text-center border border-status-red/20 bg-status-red/5 rounded-lg mx-4 my-2">
|
|
39
|
+
<p className="text-status-red text-sm font-medium mb-1">
|
|
40
|
+
Failed to render {this.props.blockType} block
|
|
41
|
+
</p>
|
|
42
|
+
<p className="text-text-3 text-xs">
|
|
43
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return this.props.children
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback for unregistered block types
|
|
53
|
+
function PlaceholderBlock({ block }: { block: BlockConfig }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="px-9 py-7 text-center text-text-3 text-sm">
|
|
56
|
+
{block.type} block (coming soon)
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const blockRenderers: Record<string, React.ComponentType<{ block: BlockConfig }>> = {
|
|
62
|
+
navbar: NavbarBlock,
|
|
63
|
+
hero: HeroBlock,
|
|
64
|
+
features: FeaturesBlock,
|
|
65
|
+
pricing: PricingBlock,
|
|
66
|
+
cta: CtaBlock,
|
|
67
|
+
footer: FooterBlock,
|
|
68
|
+
testimonials: TestimonialsBlock,
|
|
69
|
+
stats: StatsBlock,
|
|
70
|
+
faq: FaqBlock,
|
|
71
|
+
team: TeamBlock,
|
|
72
|
+
contact: ContactBlock,
|
|
73
|
+
newsletter: NewsletterBlock,
|
|
74
|
+
logocloud: LogoCloudBlock,
|
|
75
|
+
divider: DividerBlock,
|
|
76
|
+
banner: BannerBlock,
|
|
77
|
+
content: ContentBlock,
|
|
78
|
+
image: ImageBlock,
|
|
79
|
+
video: VideoBlock,
|
|
80
|
+
gallery: GalleryBlock,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function RenderBlock({ block }: { block: BlockConfig }): ReactNode {
|
|
84
|
+
const Renderer = blockRenderers[block.type] || PlaceholderBlock
|
|
85
|
+
return (
|
|
86
|
+
<BlockErrorBoundary blockType={block.type}>
|
|
87
|
+
<Renderer block={block} />
|
|
88
|
+
</BlockErrorBoundary>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
interface StatItem {
|
|
4
|
+
value: string
|
|
5
|
+
label: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface StatsProps {
|
|
9
|
+
title?: string
|
|
10
|
+
items?: StatItem[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultStats: StatItem[] = [
|
|
14
|
+
{ value: '10K+', label: 'Sites built' },
|
|
15
|
+
{ value: '99.9%', label: 'Uptime' },
|
|
16
|
+
{ value: '50ms', label: 'Avg. response' },
|
|
17
|
+
{ value: '4.9/5', label: 'User rating' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
function StatsGrid({ props }: { props: StatsProps }) {
|
|
21
|
+
const items = props.items || defaultStats
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="px-6 @md:px-10 py-12 @md:py-16">
|
|
25
|
+
{props.title && (
|
|
26
|
+
<h2 className="reveal-fade-up reveal-d1 text-xl font-bold tracking-tight text-center mb-8">{props.title}</h2>
|
|
27
|
+
)}
|
|
28
|
+
<div className="grid grid-cols-2 @2xl:grid-cols-4 gap-4">
|
|
29
|
+
{items.map((item, i) => (
|
|
30
|
+
<div key={i} className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} text-center p-4 rounded-xl bg-bg-2 border border-border-default`}>
|
|
31
|
+
<div className="text-3xl @md:text-4xl font-bold tracking-tight text-green mb-1">
|
|
32
|
+
{item.value}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="text-[12px] text-text-2 font-medium">{item.label}</div>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StatsBar({ props }: { props: StatsProps }) {
|
|
43
|
+
const items = props.items || defaultStats
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section className="px-6 @md:px-10 py-10">
|
|
47
|
+
<div className="reveal-scale reveal-d1 flex flex-wrap items-center justify-center gap-8 @md:gap-12 py-6 px-4 rounded-xl bg-bg-2 border border-border-default">
|
|
48
|
+
{items.map((item, i) => (
|
|
49
|
+
<div key={i} className="text-center">
|
|
50
|
+
<div className="text-2xl @md:text-3xl font-bold tracking-tight text-text-0 mb-0.5">
|
|
51
|
+
{item.value}
|
|
52
|
+
</div>
|
|
53
|
+
<div className="text-[11px] text-text-3 font-medium uppercase tracking-wider">
|
|
54
|
+
{item.label}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function StatsCounter({ props }: { props: StatsProps }) {
|
|
64
|
+
const items = props.items || defaultStats
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section className="px-6 @md:px-10 py-12 @md:py-16">
|
|
68
|
+
{props.title && (
|
|
69
|
+
<h2 className="reveal-fade-up reveal-d1 text-xl font-bold tracking-tight text-center mb-8">{props.title}</h2>
|
|
70
|
+
)}
|
|
71
|
+
<div className="grid grid-cols-2 @2xl:grid-cols-4 gap-4">
|
|
72
|
+
{items.map((item, i) => (
|
|
73
|
+
<div key={i} className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} text-center p-5 rounded-xl bg-green/8 border border-green/15`}>
|
|
74
|
+
<div className="text-3xl @md:text-4xl font-bold tracking-tight text-text-0 mb-1">
|
|
75
|
+
{item.value}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="text-[12px] text-green font-medium">{item.label}</div>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function StatsBlock({ block }: { block: BlockConfig }) {
|
|
86
|
+
const props = block.props as unknown as StatsProps
|
|
87
|
+
|
|
88
|
+
switch (block.variant) {
|
|
89
|
+
case 'bar':
|
|
90
|
+
return <StatsBar props={props} />
|
|
91
|
+
case 'counter':
|
|
92
|
+
return <StatsCounter props={props} />
|
|
93
|
+
default:
|
|
94
|
+
return <StatsGrid props={props} />
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BlockConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
interface TeamMember {
|
|
4
|
+
name: string
|
|
5
|
+
role: string
|
|
6
|
+
avatar?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TeamProps {
|
|
10
|
+
title?: string
|
|
11
|
+
subtitle?: string
|
|
12
|
+
members?: TeamMember[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const defaultMembers: TeamMember[] = [
|
|
16
|
+
{ name: 'Alex Rivera', role: 'CEO & Founder' },
|
|
17
|
+
{ name: 'Jordan Lee', role: 'CTO' },
|
|
18
|
+
{ name: 'Sam Patel', role: 'Head of Design' },
|
|
19
|
+
{ name: 'Casey Morgan', role: 'Lead Engineer' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export function TeamBlock({ block }: { block: BlockConfig }) {
|
|
23
|
+
const props = block.props as unknown as TeamProps
|
|
24
|
+
const members = props.members || defaultMembers
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<section className="px-6 @md:px-10 py-16 @md:py-20">
|
|
28
|
+
<div className="reveal-fade-up reveal-d1 text-center mb-10">
|
|
29
|
+
<h2 className="text-2xl @md:text-3xl font-bold tracking-tight mb-2">
|
|
30
|
+
{props.title || 'Meet the Team'}
|
|
31
|
+
</h2>
|
|
32
|
+
{props.subtitle && (
|
|
33
|
+
<p className="text-text-2 text-sm max-w-lg mx-auto">{props.subtitle}</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="grid grid-cols-2 @2xl:grid-cols-4 gap-4 max-w-3xl mx-auto">
|
|
38
|
+
{members.map((member, i) => (
|
|
39
|
+
<div key={i} className={`reveal-fade-up reveal-d${Math.min(i + 2, 8)} text-center group`}>
|
|
40
|
+
<div className="w-20 h-20 mx-auto rounded-full bg-bg-3 border-2 border-border-default flex items-center justify-center text-xl font-bold text-text-3 mb-3 transition-all group-hover:border-green/30 group-hover:accent-glow-sm">
|
|
41
|
+
{member.name.split(' ').map(n => n[0]).join('')}
|
|
42
|
+
</div>
|
|
43
|
+
<h3 className="text-sm font-semibold">{member.name}</h3>
|
|
44
|
+
<p className="text-[11px] text-text-3 mt-0.5">{member.role}</p>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
)
|
|
50
|
+
}
|