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