@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,330 @@
1
+ import type { ThemeConfig } from '@/blocks/types'
2
+
3
+ export const defaultTheme: ThemeConfig = {
4
+ bg0: '#09090b',
5
+ bg1: '#0f0f12',
6
+ bg2: '#18181b',
7
+ bg3: '#1e1e23',
8
+ bg4: '#27272a',
9
+ bg5: '#303036',
10
+ text0: '#fafafa',
11
+ text1: '#a1a1aa',
12
+ text2: '#71717a',
13
+ text3: '#52525b',
14
+ accent: '#22c55e',
15
+ accentDim: '#16a34a',
16
+ borderDefault: '#2a2a2f',
17
+ borderSubtle: '#1f1f24',
18
+ borderHover: '#3a3a3f',
19
+ fontSans: 'DM Sans',
20
+ fontDisplay: 'Space Grotesk',
21
+ fontMono: 'JetBrains Mono',
22
+ radius: 8,
23
+ radiusLg: 12,
24
+ }
25
+
26
+ export interface ThemePreset {
27
+ id: string
28
+ name: string
29
+ theme: ThemeConfig
30
+ }
31
+
32
+ export const themePresets: ThemePreset[] = [
33
+ {
34
+ id: 'default',
35
+ name: 'Dark Minimal',
36
+ theme: { ...defaultTheme },
37
+ },
38
+ {
39
+ id: 'ivory',
40
+ name: 'Ivory',
41
+ theme: {
42
+ ...defaultTheme,
43
+ bg0: '#fdfcfa',
44
+ bg1: '#f5f3ef',
45
+ bg2: '#edebe5',
46
+ bg3: '#e0ddd6',
47
+ bg4: '#ccc8bf',
48
+ bg5: '#b0ab9f',
49
+ text0: '#1c1917',
50
+ text1: '#44403c',
51
+ text2: '#78716c',
52
+ text3: '#a8a29e',
53
+ accent: '#4f46e5',
54
+ accentDim: '#4338ca',
55
+ borderDefault: '#e5e2db',
56
+ borderSubtle: '#f0ede7',
57
+ borderHover: '#d5d0c8',
58
+ fontSans: 'Plus Jakarta Sans',
59
+ fontDisplay: 'Sora',
60
+ radius: 10,
61
+ radiusLg: 16,
62
+ },
63
+ },
64
+ {
65
+ id: 'clean',
66
+ name: 'Clean',
67
+ theme: {
68
+ ...defaultTheme,
69
+ bg0: '#ffffff',
70
+ bg1: '#f8f9fa',
71
+ bg2: '#f1f3f5',
72
+ bg3: '#e9ecef',
73
+ bg4: '#dee2e6',
74
+ bg5: '#ced4da',
75
+ text0: '#212529',
76
+ text1: '#495057',
77
+ text2: '#868e96',
78
+ text3: '#adb5bd',
79
+ accent: '#228be6',
80
+ accentDim: '#1c7ed6',
81
+ borderDefault: '#e9ecef',
82
+ borderSubtle: '#f1f3f5',
83
+ borderHover: '#dee2e6',
84
+ fontSans: 'Inter',
85
+ fontDisplay: 'Inter',
86
+ radius: 8,
87
+ radiusLg: 12,
88
+ },
89
+ },
90
+ {
91
+ id: 'sand',
92
+ name: 'Sand',
93
+ theme: {
94
+ ...defaultTheme,
95
+ bg0: '#faf8f5',
96
+ bg1: '#f2efe8',
97
+ bg2: '#e8e4db',
98
+ bg3: '#ddd8cd',
99
+ bg4: '#c8c2b5',
100
+ bg5: '#b3ab9c',
101
+ text0: '#2c2418',
102
+ text1: '#5c5347',
103
+ text2: '#8a8074',
104
+ text3: '#b0a798',
105
+ accent: '#d97706',
106
+ accentDim: '#b45309',
107
+ borderDefault: '#e2ddd4',
108
+ borderSubtle: '#ebe8e1',
109
+ borderHover: '#d5cfc4',
110
+ fontSans: 'Nunito Sans',
111
+ fontDisplay: 'Outfit',
112
+ radius: 10,
113
+ radiusLg: 14,
114
+ },
115
+ },
116
+ {
117
+ id: 'amber',
118
+ name: 'Amber',
119
+ theme: {
120
+ ...defaultTheme,
121
+ bg0: '#171210',
122
+ bg1: '#1e1816',
123
+ bg2: '#26201c',
124
+ bg3: '#302925',
125
+ bg4: '#3d3530',
126
+ bg5: '#504640',
127
+ text0: '#faf6f0',
128
+ text1: '#d4c8b8',
129
+ text2: '#a89a88',
130
+ text3: '#7d7062',
131
+ accent: '#e8a838',
132
+ accentDim: '#cc8f20',
133
+ borderDefault: '#352e28',
134
+ borderSubtle: '#28211b',
135
+ borderHover: '#443c35',
136
+ fontSans: 'Outfit',
137
+ fontDisplay: 'Outfit',
138
+ radius: 6,
139
+ radiusLg: 10,
140
+ },
141
+ },
142
+ {
143
+ id: 'ocean',
144
+ name: 'Ocean',
145
+ theme: {
146
+ ...defaultTheme,
147
+ bg0: '#0a1628',
148
+ bg1: '#101e32',
149
+ bg2: '#162740',
150
+ bg3: '#1d3050',
151
+ bg4: '#264060',
152
+ bg5: '#305575',
153
+ text0: '#eef4ff',
154
+ text1: '#b0c8e8',
155
+ text2: '#7a9cc0',
156
+ text3: '#506d90',
157
+ accent: '#3b82f6',
158
+ accentDim: '#2563eb',
159
+ borderDefault: '#1e3250',
160
+ borderSubtle: '#162640',
161
+ borderHover: '#2a4468',
162
+ fontSans: 'Inter',
163
+ fontDisplay: 'Sora',
164
+ },
165
+ },
166
+ {
167
+ id: 'rose',
168
+ name: 'Rosé',
169
+ theme: {
170
+ ...defaultTheme,
171
+ bg0: '#16101a',
172
+ bg1: '#1e1620',
173
+ bg2: '#261e28',
174
+ bg3: '#302632',
175
+ bg4: '#3e3340',
176
+ bg5: '#504250',
177
+ text0: '#faf4f8',
178
+ text1: '#d0bcc8',
179
+ text2: '#a08898',
180
+ text3: '#786575',
181
+ accent: '#f472b6',
182
+ accentDim: '#ec4899',
183
+ borderDefault: '#30252e',
184
+ borderSubtle: '#241c25',
185
+ borderHover: '#402e3a',
186
+ fontSans: 'DM Sans',
187
+ fontDisplay: 'Raleway',
188
+ radius: 12,
189
+ radiusLg: 18,
190
+ },
191
+ },
192
+ {
193
+ id: 'purple-haze',
194
+ name: 'Purple Haze',
195
+ theme: {
196
+ ...defaultTheme,
197
+ bg0: '#100c1a',
198
+ bg1: '#16111f',
199
+ bg2: '#1e1828',
200
+ bg3: '#262033',
201
+ bg4: '#302a40',
202
+ bg5: '#3e3650',
203
+ text0: '#f5f0ff',
204
+ text1: '#c4b5fd',
205
+ text2: '#8b7cbf',
206
+ text3: '#6b5c8f',
207
+ accent: '#8b5cf6',
208
+ accentDim: '#7c3aed',
209
+ borderDefault: '#2a2240',
210
+ borderSubtle: '#1e1832',
211
+ borderHover: '#38304f',
212
+ fontSans: 'Manrope',
213
+ fontDisplay: 'Sora',
214
+ radius: 10,
215
+ radiusLg: 14,
216
+ },
217
+ },
218
+ {
219
+ id: 'slate',
220
+ name: 'Slate',
221
+ theme: {
222
+ ...defaultTheme,
223
+ bg0: '#0f1114',
224
+ bg1: '#161a1e',
225
+ bg2: '#1e2228',
226
+ bg3: '#252a32',
227
+ bg4: '#2e353e',
228
+ bg5: '#3a4250',
229
+ text0: '#f0f4f8',
230
+ text1: '#94a3b8',
231
+ text2: '#64748b',
232
+ text3: '#475569',
233
+ accent: '#06b6d4',
234
+ accentDim: '#0891b2',
235
+ borderDefault: '#252a32',
236
+ borderSubtle: '#1c2028',
237
+ borderHover: '#323a45',
238
+ fontSans: 'Work Sans',
239
+ fontDisplay: 'Space Grotesk',
240
+ radius: 6,
241
+ radiusLg: 10,
242
+ },
243
+ },
244
+ {
245
+ id: 'forest',
246
+ name: 'Forest',
247
+ theme: {
248
+ ...defaultTheme,
249
+ bg0: '#0a120c',
250
+ bg1: '#101a13',
251
+ bg2: '#16221a',
252
+ bg3: '#1e2c22',
253
+ bg4: '#26382a',
254
+ bg5: '#304434',
255
+ text0: '#eef8f0',
256
+ text1: '#a0c8a8',
257
+ text2: '#6ea078',
258
+ text3: '#4e7858',
259
+ accent: '#10b981',
260
+ accentDim: '#059669',
261
+ borderDefault: '#1e3022',
262
+ borderSubtle: '#162418',
263
+ borderHover: '#2c4230',
264
+ fontSans: 'Plus Jakarta Sans',
265
+ fontDisplay: 'DM Sans',
266
+ radius: 8,
267
+ radiusLg: 14,
268
+ },
269
+ },
270
+ ]
271
+
272
+ export function resolveTheme(partial?: Partial<ThemeConfig>): ThemeConfig {
273
+ if (!partial) return defaultTheme
274
+ return { ...defaultTheme, ...partial }
275
+ }
276
+
277
+ export function hexToRgb(hex: string): string {
278
+ const h = hex.replace('#', '')
279
+ const r = parseInt(h.slice(0, 2), 16)
280
+ const g = parseInt(h.slice(2, 4), 16)
281
+ const b = parseInt(h.slice(4, 6), 16)
282
+ return `${r}, ${g}, ${b}`
283
+ }
284
+
285
+ export function themeToCSS(theme: ThemeConfig): Record<string, string> {
286
+ const rgb = hexToRgb(theme.accent)
287
+ return {
288
+ '--color-bg-0': theme.bg0,
289
+ '--color-bg-1': theme.bg1,
290
+ '--color-bg-2': theme.bg2,
291
+ '--color-bg-3': theme.bg3,
292
+ '--color-bg-4': theme.bg4,
293
+ '--color-bg-5': theme.bg5,
294
+ '--color-text-0': theme.text0,
295
+ '--color-text-1': theme.text1,
296
+ '--color-text-2': theme.text2,
297
+ '--color-text-3': theme.text3,
298
+ '--color-green': theme.accent,
299
+ '--color-green-dim': theme.accentDim,
300
+ '--color-green-glow': `rgba(${rgb}, 0.12)`,
301
+ '--color-green-glow2': `rgba(${rgb}, 0.06)`,
302
+ '--color-border-default': theme.borderDefault,
303
+ '--color-border-subtle': theme.borderSubtle,
304
+ '--color-border-hover': theme.borderHover,
305
+ '--color-accent-rgb': rgb,
306
+ '--font-sans': `"${theme.fontSans}", -apple-system, system-ui, sans-serif`,
307
+ '--font-display': `"${theme.fontDisplay}", system-ui, sans-serif`,
308
+ '--font-mono': `"${theme.fontMono}", ui-monospace, monospace`,
309
+ '--radius-default': `${theme.radius}px`,
310
+ '--radius-lg': `${theme.radiusLg}px`,
311
+ }
312
+ }
313
+
314
+ export const googleFontOptions = [
315
+ 'DM Sans',
316
+ 'Inter',
317
+ 'Space Grotesk',
318
+ 'Poppins',
319
+ 'Manrope',
320
+ 'Outfit',
321
+ 'Plus Jakarta Sans',
322
+ 'Sora',
323
+ 'Nunito Sans',
324
+ 'Work Sans',
325
+ 'Rubik',
326
+ 'Raleway',
327
+ 'JetBrains Mono',
328
+ 'Fira Code',
329
+ 'Source Code Pro',
330
+ ]
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ const DEFAULT_FONTS = new Set(['DM Sans', 'JetBrains Mono', 'Space Grotesk'])
4
+ const loadedFonts = new Set<string>()
5
+
6
+ function fontToGoogleUrl(fontName: string): string {
7
+ const family = fontName.replace(/ /g, '+')
8
+ return `https://fonts.googleapis.com/css2?family=${family}:wght@300;400;500;600;700&display=swap`
9
+ }
10
+
11
+ export function useGoogleFonts(fonts: string[]) {
12
+ const prevRef = useRef<string[]>([])
13
+
14
+ useEffect(() => {
15
+ const unique = [...new Set(fonts)].filter(
16
+ (f) => f && !DEFAULT_FONTS.has(f) && !loadedFonts.has(f)
17
+ )
18
+
19
+ if (unique.length === 0) return
20
+
21
+ const links: HTMLLinkElement[] = []
22
+
23
+ for (const font of unique) {
24
+ if (document.querySelector(`link[data-openpage-font="${font}"]`)) {
25
+ loadedFonts.add(font)
26
+ continue
27
+ }
28
+
29
+ const link = document.createElement('link')
30
+ link.rel = 'stylesheet'
31
+ link.href = fontToGoogleUrl(font)
32
+ link.setAttribute('data-openpage-font', font)
33
+ document.head.appendChild(link)
34
+ links.push(link)
35
+ loadedFonts.add(font)
36
+
37
+ // Preload the font to prevent FOUT
38
+ document.fonts.load(`16px "${font}"`).catch(() => {
39
+ // Font load failed, no-op (CSS fallback will handle it)
40
+ })
41
+ }
42
+
43
+ prevRef.current = fonts
44
+
45
+ return () => {
46
+ // Don't remove font links on unmount, they're cached and shared
47
+ }
48
+ }, [fonts.join(',')]) // eslint-disable-line react-hooks/exhaustive-deps
49
+ }
@@ -0,0 +1,28 @@
1
+ import { useRef, useState, useEffect } from 'react'
2
+
3
+ export function useScrollReveal(disabled = false) {
4
+ const ref = useRef<HTMLDivElement>(null)
5
+ const [isRevealed, setIsRevealed] = useState(disabled)
6
+
7
+ useEffect(() => {
8
+ if (disabled) return
9
+
10
+ const el = ref.current
11
+ if (!el) return
12
+
13
+ const observer = new IntersectionObserver(
14
+ ([entry]) => {
15
+ if (entry.isIntersecting) {
16
+ setIsRevealed(true)
17
+ observer.disconnect()
18
+ }
19
+ },
20
+ { threshold: 0.15, rootMargin: '-60px' },
21
+ )
22
+
23
+ observer.observe(el)
24
+ return () => observer.disconnect()
25
+ }, [disabled])
26
+
27
+ return { ref, isRevealed: disabled || isRevealed }
28
+ }
@@ -0,0 +1,82 @@
1
+ import { useState } from 'react'
2
+ import { toast } from 'sonner'
3
+
4
+ /**
5
+ * Gemini API Key FieldGroup — BYOK (bring-your-own-key) input with show/hide and a
6
+ * live validation button that pings the Google Gemini REST API.
7
+ *
8
+ * Extracted from src/routes/Settings.tsx ApiPanel (Gemini FieldGroup only).
9
+ * - Local state for show/hide + testing in-flight flag.
10
+ * - Persists the key in localStorage under `openpage-gemini-key`; cleared when empty.
11
+ * - Validates by GET https://generativelanguage.googleapis.com/v1beta/models?key=...
12
+ * - `toast` (sonner) is the only external dependency preserved from the inline source.
13
+ */
14
+ export function GeminiKeyInputField() {
15
+ const [geminiKey, setGeminiKey] = useState(() => localStorage.getItem('openpage-gemini-key') || '')
16
+ const [showKey, setShowKey] = useState(false)
17
+ const [testing, setTesting] = useState(false)
18
+
19
+ function handleKeyChange(value: string) {
20
+ setGeminiKey(value)
21
+ if (value) {
22
+ localStorage.setItem('openpage-gemini-key', value)
23
+ } else {
24
+ localStorage.removeItem('openpage-gemini-key')
25
+ }
26
+ }
27
+
28
+ async function handleTest() {
29
+ if (!geminiKey) return
30
+ setTesting(true)
31
+ try {
32
+ const res = await fetch(
33
+ `https://generativelanguage.googleapis.com/v1beta/models?key=${geminiKey}`,
34
+ )
35
+ if (res.ok) {
36
+ toast.success('API key is valid')
37
+ } else {
38
+ toast.error(`Invalid key: ${res.status}`)
39
+ }
40
+ } catch {
41
+ toast.error('Connection failed')
42
+ } finally {
43
+ setTesting(false)
44
+ }
45
+ }
46
+
47
+ return (
48
+ <div className="mb-5">
49
+ <label className="block text-[11.5px] text-text-2 mb-1.5 font-medium">
50
+ Gemini API Key
51
+ </label>
52
+ <div className="flex gap-2">
53
+ <input
54
+ type={showKey ? 'text' : 'password'}
55
+ value={geminiKey}
56
+ placeholder="AIza..."
57
+ onChange={(e) => handleKeyChange(e.target.value)}
58
+ className="flex-1 px-3 py-2 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 font-mono"
59
+ />
60
+ <button
61
+ type="button"
62
+ onClick={() => setShowKey(!showKey)}
63
+ className="px-3 py-2 rounded-lg border border-border-default bg-bg-2 text-text-2 text-[12px] hover:text-text-0 hover:bg-bg-3 transition-colors shrink-0"
64
+ >
65
+ {showKey ? 'Hide' : 'Show'}
66
+ </button>
67
+ <button
68
+ type="button"
69
+ onClick={handleTest}
70
+ disabled={!geminiKey || testing}
71
+ className="px-3 py-2 rounded-lg bg-green/10 text-green text-[12px] font-medium hover:bg-green/20 transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
72
+ >
73
+ {testing ? 'Testing...' : 'Test'}
74
+ </button>
75
+ </div>
76
+ <p className="text-[11px] text-text-3 mt-1.5">
77
+ Used for client-side AI generation. Get one at{' '}
78
+ <span className="text-text-2">aistudio.google.com</span>
79
+ </p>
80
+ </div>
81
+ )
82
+ }