@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,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
+ }