@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +4 -0
  3. package/README.md +82 -0
  4. package/dist/index.d.ts +401 -0
  5. package/dist/index.js +5389 -0
  6. package/package.json +66 -0
  7. package/src/blocks/BlockWrapper.tsx +149 -0
  8. package/src/blocks/banner/BannerBlock.tsx +38 -0
  9. package/src/blocks/contact/ContactBlock.tsx +63 -0
  10. package/src/blocks/content/ContentBlock.tsx +38 -0
  11. package/src/blocks/cta/CtaBlock.tsx +67 -0
  12. package/src/blocks/divider/DividerBlock.tsx +30 -0
  13. package/src/blocks/faq/FaqBlock.tsx +82 -0
  14. package/src/blocks/features/FeaturesBlock.tsx +170 -0
  15. package/src/blocks/footer/FooterBlock.tsx +136 -0
  16. package/src/blocks/gallery/GalleryBlock.tsx +67 -0
  17. package/src/blocks/hero/HeroBlock.tsx +163 -0
  18. package/src/blocks/image/ImageBlock.tsx +78 -0
  19. package/src/blocks/logocloud/LogoCloudBlock.tsx +70 -0
  20. package/src/blocks/navbar/NavbarBlock.tsx +102 -0
  21. package/src/blocks/newsletter/NewsletterBlock.tsx +51 -0
  22. package/src/blocks/pricing/PricingBlock.tsx +173 -0
  23. package/src/blocks/registry.tsx +90 -0
  24. package/src/blocks/stats/StatsBlock.tsx +96 -0
  25. package/src/blocks/team/TeamBlock.tsx +50 -0
  26. package/src/blocks/testimonials/TestimonialsBlock.tsx +203 -0
  27. package/src/blocks/types.ts +72 -0
  28. package/src/blocks/video/VideoBlock.tsx +58 -0
  29. package/src/editor/AgentPanel.tsx +318 -0
  30. package/src/editor/Canvas.tsx +95 -0
  31. package/src/editor/CanvasEmpty.tsx +40 -0
  32. package/src/editor/CanvasToolbar.tsx +366 -0
  33. package/src/editor/DesignPanel.tsx +224 -0
  34. package/src/editor/EditorLayout.tsx +196 -0
  35. package/src/editor/GenerationOverlay.tsx +201 -0
  36. package/src/editor/JsonDrawer.tsx +139 -0
  37. package/src/editor/LayersPanel.tsx +259 -0
  38. package/src/editor/LeftSidebar.tsx +136 -0
  39. package/src/editor/PropertiesPanel.tsx +564 -0
  40. package/src/editor/RightSidebar.tsx +85 -0
  41. package/src/editor/ShortcutsModal.tsx +126 -0
  42. package/src/editor/VersionHistory.tsx +110 -0
  43. package/src/index.ts +133 -0
  44. package/src/lib/block-metadata.ts +167 -0
  45. package/src/lib/export-html.ts +1424 -0
  46. package/src/lib/generate-site.ts +206 -0
  47. package/src/lib/generation-prompt.ts +64 -0
  48. package/src/lib/markdown.ts +88 -0
  49. package/src/lib/templates.ts +139 -0
  50. package/src/lib/theme-presets.ts +330 -0
  51. package/src/lib/useGoogleFonts.ts +49 -0
  52. package/src/lib/useScrollReveal.ts +28 -0
  53. package/src/settings/GeminiKeyInputField.tsx +82 -0
  54. package/src/store/configStore.ts +344 -0
  55. package/src/store/editorStore.ts +50 -0
  56. 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
+ }