@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,126 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { X } from 'lucide-react'
|
|
3
|
+
import { useEditorStore } from '@/store/editorStore'
|
|
4
|
+
|
|
5
|
+
const shortcutGroups = [
|
|
6
|
+
{
|
|
7
|
+
title: 'Navigation',
|
|
8
|
+
shortcuts: [
|
|
9
|
+
{ keys: ['1'], description: 'Dashboard' },
|
|
10
|
+
{ keys: ['2'], description: 'Editor' },
|
|
11
|
+
{ keys: ['3'], description: 'Components' },
|
|
12
|
+
{ keys: ['4'], description: 'Deploy' },
|
|
13
|
+
{ keys: ['5'], description: 'Settings' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Editor',
|
|
18
|
+
shortcuts: [
|
|
19
|
+
{ keys: ['J'], description: 'Toggle JSON drawer' },
|
|
20
|
+
{ keys: ['H'], description: 'Toggle version history' },
|
|
21
|
+
{ keys: ['P'], description: 'Toggle preview mode' },
|
|
22
|
+
{ keys: ['Esc'], description: 'Deselect block' },
|
|
23
|
+
{ keys: ['?'], description: 'Show this help' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: 'Actions',
|
|
28
|
+
shortcuts: [
|
|
29
|
+
{ keys: ['\u2318', 'Z'], description: 'Undo' },
|
|
30
|
+
{ keys: ['\u2318', '\u21E7', 'Z'], description: 'Redo' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export function ShortcutsModal() {
|
|
36
|
+
const { shortcutsModalOpen, toggleShortcutsModal } = useEditorStore()
|
|
37
|
+
const modalRef = useRef<HTMLDivElement>(null)
|
|
38
|
+
const closeRef = useRef<HTMLButtonElement>(null)
|
|
39
|
+
|
|
40
|
+
// Focus trap + auto-focus close button
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!shortcutsModalOpen) return
|
|
43
|
+
closeRef.current?.focus()
|
|
44
|
+
}, [shortcutsModalOpen])
|
|
45
|
+
|
|
46
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
47
|
+
if (e.key === 'Escape') {
|
|
48
|
+
toggleShortcutsModal()
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
if (e.key !== 'Tab') return
|
|
52
|
+
const modal = modalRef.current
|
|
53
|
+
if (!modal) return
|
|
54
|
+
const focusable = modal.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
55
|
+
if (focusable.length === 0) return
|
|
56
|
+
const first = focusable[0]
|
|
57
|
+
const last = focusable[focusable.length - 1]
|
|
58
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
last.focus()
|
|
61
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
first.focus()
|
|
64
|
+
}
|
|
65
|
+
}, [toggleShortcutsModal])
|
|
66
|
+
|
|
67
|
+
if (!shortcutsModalOpen) return null
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
72
|
+
onClick={toggleShortcutsModal}
|
|
73
|
+
role="dialog"
|
|
74
|
+
aria-modal="true"
|
|
75
|
+
aria-label="Keyboard shortcuts"
|
|
76
|
+
onKeyDown={handleKeyDown}
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
ref={modalRef}
|
|
80
|
+
className="bg-bg-1 border border-border-default rounded-xl w-[420px] max-h-[80vh] overflow-hidden shadow-[0_16px_48px_rgba(0,0,0,0.5)]"
|
|
81
|
+
onClick={(e) => e.stopPropagation()}
|
|
82
|
+
>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<div className="px-5 py-4 border-b border-border-default flex items-center justify-between">
|
|
85
|
+
<h2 className="text-sm font-semibold">Keyboard Shortcuts</h2>
|
|
86
|
+
<button
|
|
87
|
+
ref={closeRef}
|
|
88
|
+
onClick={toggleShortcutsModal}
|
|
89
|
+
className="w-7 h-7 rounded flex items-center justify-center text-text-3 hover:text-text-0 hover:bg-bg-3 transition-colors"
|
|
90
|
+
aria-label="Close shortcuts"
|
|
91
|
+
>
|
|
92
|
+
<X size={14} />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Body */}
|
|
97
|
+
<div className="p-5 space-y-5 overflow-y-auto">
|
|
98
|
+
{shortcutGroups.map((group) => (
|
|
99
|
+
<div key={group.title}>
|
|
100
|
+
<h3 className="text-[10px] font-semibold uppercase tracking-wider text-text-3 mb-2">
|
|
101
|
+
{group.title}
|
|
102
|
+
</h3>
|
|
103
|
+
<div className="space-y-1.5">
|
|
104
|
+
{group.shortcuts.map((shortcut) => (
|
|
105
|
+
<div key={shortcut.description} className="flex items-center justify-between">
|
|
106
|
+
<span className="text-[12.5px] text-text-1">{shortcut.description}</span>
|
|
107
|
+
<div className="flex gap-1">
|
|
108
|
+
{shortcut.keys.map((key, i) => (
|
|
109
|
+
<kbd
|
|
110
|
+
key={i}
|
|
111
|
+
className="min-w-[24px] h-6 px-1.5 rounded border border-border-default bg-bg-3 text-[11px] font-mono text-text-2 flex items-center justify-center"
|
|
112
|
+
>
|
|
113
|
+
{key}
|
|
114
|
+
</kbd>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { X, Clock, RotateCcw } from 'lucide-react'
|
|
3
|
+
import { useEditorStore } from '@/store/editorStore'
|
|
4
|
+
import { useConfigStore } from '@/store/configStore'
|
|
5
|
+
|
|
6
|
+
function timeAgo(ts: number): string {
|
|
7
|
+
const seconds = Math.floor((Date.now() - ts) / 1000)
|
|
8
|
+
if (seconds < 10) return 'Just now'
|
|
9
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
10
|
+
const minutes = Math.floor(seconds / 60)
|
|
11
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
12
|
+
const hours = Math.floor(minutes / 60)
|
|
13
|
+
return `${hours}h ago`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function VersionHistory() {
|
|
17
|
+
const { historyOpen, toggleHistory } = useEditorStore()
|
|
18
|
+
const undoStack = useConfigStore((s) => s.undoStack)
|
|
19
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
|
|
21
|
+
const entries = [...undoStack].reverse()
|
|
22
|
+
|
|
23
|
+
// Close on Escape
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!historyOpen) return
|
|
26
|
+
function handleKey(e: KeyboardEvent) {
|
|
27
|
+
if (e.key === 'Escape') { e.preventDefault(); toggleHistory() }
|
|
28
|
+
}
|
|
29
|
+
window.addEventListener('keydown', handleKey)
|
|
30
|
+
return () => window.removeEventListener('keydown', handleKey)
|
|
31
|
+
}, [historyOpen, toggleHistory])
|
|
32
|
+
|
|
33
|
+
// Close on click outside
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!historyOpen) return
|
|
36
|
+
function handleClick(e: MouseEvent) {
|
|
37
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
38
|
+
toggleHistory()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Delay to avoid catching the toggle button click itself
|
|
42
|
+
const id = setTimeout(() => window.addEventListener('mousedown', handleClick), 0)
|
|
43
|
+
return () => { clearTimeout(id); window.removeEventListener('mousedown', handleClick) }
|
|
44
|
+
}, [historyOpen, toggleHistory])
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
ref={panelRef}
|
|
49
|
+
className={`absolute top-0 right-0 bottom-0 w-80 bg-bg-1 border-l border-border-default z-50 flex flex-col transition-transform duration-250 ease-in-out ${
|
|
50
|
+
historyOpen ? 'translate-x-0' : 'translate-x-full'
|
|
51
|
+
}`}
|
|
52
|
+
>
|
|
53
|
+
{/* Header */}
|
|
54
|
+
<div className="px-4 py-3.5 border-b border-border-default flex items-center justify-between">
|
|
55
|
+
<div className="flex items-center gap-2">
|
|
56
|
+
<Clock size={14} className="text-text-2" />
|
|
57
|
+
<h3 className="text-sm font-semibold">Version History</h3>
|
|
58
|
+
<span className="text-[10px] text-text-3">({entries.length})</span>
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
onClick={toggleHistory}
|
|
62
|
+
className="w-7 h-7 rounded flex items-center justify-center text-text-3 hover:text-text-0 hover:bg-bg-3 transition-colors"
|
|
63
|
+
>
|
|
64
|
+
<X size={14} />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* History list */}
|
|
69
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
70
|
+
{/* Current state */}
|
|
71
|
+
<div className="p-3 rounded-lg bg-green-glow mb-0.5">
|
|
72
|
+
<div className="flex items-center gap-1.5 text-[11px] text-text-2 mb-1">
|
|
73
|
+
<span>Current</span>
|
|
74
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded-full font-semibold bg-green-glow text-green">
|
|
75
|
+
latest
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="text-[12.5px] text-text-1">Current state</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{entries.map((entry, i) => (
|
|
82
|
+
<div
|
|
83
|
+
key={i}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
for (let n = 0; n <= i; n++) useConfigStore.getState().undo()
|
|
86
|
+
}}
|
|
87
|
+
className="p-3 rounded-lg cursor-pointer transition-colors mb-0.5 hover:bg-bg-3 group"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex items-center gap-1.5 text-[11px] text-text-2 mb-1">
|
|
90
|
+
<span>{timeAgo(entry.timestamp)}</span>
|
|
91
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded-full font-semibold bg-status-blue/10 text-status-blue">
|
|
92
|
+
manual
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<div className="text-[12.5px] text-text-1">{entry.label}</div>
|
|
97
|
+
<RotateCcw size={12} className="text-text-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
|
|
102
|
+
{entries.length === 0 && (
|
|
103
|
+
<div className="p-4 text-center text-[11px] text-text-3">
|
|
104
|
+
No history yet. Make some changes to see history.
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @siteable/core — public barrel
|
|
3
|
+
*
|
|
4
|
+
* Surface every public named export from each module that ships with the package.
|
|
5
|
+
* Consumers MUST import through this entry (NF-002: no `exports` field, but
|
|
6
|
+
* convention-enforced — deep imports like `@siteable/core/src/store/configStore`
|
|
7
|
+
* would instantiate a second zustand singleton inside one app, breaking the
|
|
8
|
+
* single-store invariant when both private app + package copy coexist).
|
|
9
|
+
*
|
|
10
|
+
* SC-001: enumerate every named export, with `validateSiteConfig` explicit.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Blocks ──────────────────────────────────────────────────────────────────
|
|
14
|
+
export {
|
|
15
|
+
// types
|
|
16
|
+
type BlockType,
|
|
17
|
+
type BlockVariant,
|
|
18
|
+
type BlockConfig,
|
|
19
|
+
type ThemeConfig,
|
|
20
|
+
type PageConfig,
|
|
21
|
+
type SiteConfig,
|
|
22
|
+
} from './blocks/types'
|
|
23
|
+
|
|
24
|
+
export { RenderBlock } from './blocks/registry'
|
|
25
|
+
export { BlockWrapper } from './blocks/BlockWrapper'
|
|
26
|
+
|
|
27
|
+
// Individual block components (all variants consolidated into single Block components)
|
|
28
|
+
export { NavbarBlock } from './blocks/navbar/NavbarBlock'
|
|
29
|
+
export { HeroBlock } from './blocks/hero/HeroBlock'
|
|
30
|
+
export { FeaturesBlock } from './blocks/features/FeaturesBlock'
|
|
31
|
+
export { PricingBlock } from './blocks/pricing/PricingBlock'
|
|
32
|
+
export { CtaBlock } from './blocks/cta/CtaBlock'
|
|
33
|
+
export { FooterBlock } from './blocks/footer/FooterBlock'
|
|
34
|
+
export { TestimonialsBlock } from './blocks/testimonials/TestimonialsBlock'
|
|
35
|
+
export { StatsBlock } from './blocks/stats/StatsBlock'
|
|
36
|
+
export { FaqBlock } from './blocks/faq/FaqBlock'
|
|
37
|
+
export { TeamBlock } from './blocks/team/TeamBlock'
|
|
38
|
+
export { ContactBlock } from './blocks/contact/ContactBlock'
|
|
39
|
+
export { NewsletterBlock } from './blocks/newsletter/NewsletterBlock'
|
|
40
|
+
export { LogoCloudBlock } from './blocks/logocloud/LogoCloudBlock'
|
|
41
|
+
export { DividerBlock } from './blocks/divider/DividerBlock'
|
|
42
|
+
export { BannerBlock } from './blocks/banner/BannerBlock'
|
|
43
|
+
export { ContentBlock } from './blocks/content/ContentBlock'
|
|
44
|
+
export { ImageBlock } from './blocks/image/ImageBlock'
|
|
45
|
+
export { VideoBlock } from './blocks/video/VideoBlock'
|
|
46
|
+
export { GalleryBlock } from './blocks/gallery/GalleryBlock'
|
|
47
|
+
|
|
48
|
+
// ── Editor ──────────────────────────────────────────────────────────────────
|
|
49
|
+
export { AgentPanel } from './editor/AgentPanel'
|
|
50
|
+
export { Canvas } from './editor/Canvas'
|
|
51
|
+
export { CanvasEmpty } from './editor/CanvasEmpty'
|
|
52
|
+
export {
|
|
53
|
+
CanvasToolbar,
|
|
54
|
+
type CanvasToolbarProps,
|
|
55
|
+
} from './editor/CanvasToolbar'
|
|
56
|
+
export { DesignPanel } from './editor/DesignPanel'
|
|
57
|
+
export {
|
|
58
|
+
EditorLayout,
|
|
59
|
+
type EditorLayoutProps,
|
|
60
|
+
} from './editor/EditorLayout'
|
|
61
|
+
export { GenerationOverlay } from './editor/GenerationOverlay'
|
|
62
|
+
export { JsonDrawer } from './editor/JsonDrawer'
|
|
63
|
+
export { LayersPanel } from './editor/LayersPanel'
|
|
64
|
+
export { LeftSidebar } from './editor/LeftSidebar'
|
|
65
|
+
export { PropertiesPanel } from './editor/PropertiesPanel'
|
|
66
|
+
export { RightSidebar } from './editor/RightSidebar'
|
|
67
|
+
export { ShortcutsModal } from './editor/ShortcutsModal'
|
|
68
|
+
export { VersionHistory } from './editor/VersionHistory'
|
|
69
|
+
|
|
70
|
+
// ── Stores (zustand module singletons — single instance per app) ────────────
|
|
71
|
+
export {
|
|
72
|
+
defaultConfig,
|
|
73
|
+
useConfigStore,
|
|
74
|
+
} from './store/configStore'
|
|
75
|
+
export {
|
|
76
|
+
useEditorStore,
|
|
77
|
+
type Viewport,
|
|
78
|
+
} from './store/editorStore'
|
|
79
|
+
|
|
80
|
+
// ── Lib — block metadata ────────────────────────────────────────────────────
|
|
81
|
+
export {
|
|
82
|
+
type BlockMeta,
|
|
83
|
+
blockMetadata,
|
|
84
|
+
categories,
|
|
85
|
+
} from './lib/block-metadata'
|
|
86
|
+
|
|
87
|
+
// ── Lib — export-html (HTML serialization + helpers) ────────────────────────
|
|
88
|
+
export {
|
|
89
|
+
type ExportSiteSettings,
|
|
90
|
+
type ExportSiteOptions,
|
|
91
|
+
exportSiteToHTML,
|
|
92
|
+
exportToHTML,
|
|
93
|
+
downloadHTML,
|
|
94
|
+
previewHTML,
|
|
95
|
+
} from './lib/export-html'
|
|
96
|
+
|
|
97
|
+
// ── Lib — generate-site (Gemini + injectable server fallback) ───────────────
|
|
98
|
+
export {
|
|
99
|
+
type GenerationResult,
|
|
100
|
+
generateSiteConfig,
|
|
101
|
+
validateSiteConfig,
|
|
102
|
+
} from './lib/generate-site'
|
|
103
|
+
|
|
104
|
+
// ── Lib — generation-prompt (system prompt for Gemini) ──────────────────────
|
|
105
|
+
export { GENERATION_PROMPT } from './lib/generation-prompt'
|
|
106
|
+
|
|
107
|
+
// ── Lib — markdown ──────────────────────────────────────────────────────────
|
|
108
|
+
export { renderMarkdown } from './lib/markdown'
|
|
109
|
+
|
|
110
|
+
// ── Lib — templates (smart-fallback site templates) ─────────────────────────
|
|
111
|
+
export {
|
|
112
|
+
templateMeta,
|
|
113
|
+
buildTemplate,
|
|
114
|
+
getTemplateForPrompt,
|
|
115
|
+
} from './lib/templates'
|
|
116
|
+
|
|
117
|
+
// ── Lib — theme-presets (10 themes + utilities) ─────────────────────────────
|
|
118
|
+
export {
|
|
119
|
+
defaultTheme,
|
|
120
|
+
type ThemePreset,
|
|
121
|
+
themePresets,
|
|
122
|
+
resolveTheme,
|
|
123
|
+
hexToRgb,
|
|
124
|
+
themeToCSS,
|
|
125
|
+
googleFontOptions,
|
|
126
|
+
} from './lib/theme-presets'
|
|
127
|
+
|
|
128
|
+
// ── Lib — react hooks ───────────────────────────────────────────────────────
|
|
129
|
+
export { useGoogleFonts } from './lib/useGoogleFonts'
|
|
130
|
+
export { useScrollReveal } from './lib/useScrollReveal'
|
|
131
|
+
|
|
132
|
+
// ── Settings (extracted components) ─────────────────────────────────────────
|
|
133
|
+
export { GeminiKeyInputField } from './settings/GeminiKeyInputField'
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { BlockType } from '@/blocks/types'
|
|
2
|
+
|
|
3
|
+
export interface BlockMeta {
|
|
4
|
+
type: BlockType
|
|
5
|
+
label: string
|
|
6
|
+
description: string
|
|
7
|
+
category: string
|
|
8
|
+
variants: string[]
|
|
9
|
+
defaultProps: Record<string, unknown>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const blockMetadata: BlockMeta[] = [
|
|
13
|
+
{
|
|
14
|
+
type: 'navbar',
|
|
15
|
+
label: 'Navbar',
|
|
16
|
+
description: 'Navigation bar with logo, links, and CTA',
|
|
17
|
+
category: 'Navigation',
|
|
18
|
+
variants: ['default', 'centered'],
|
|
19
|
+
defaultProps: { logo: 'Brand', links: ['Features', 'Pricing', 'About'], ctaText: 'Get Started' },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'hero',
|
|
23
|
+
label: 'Hero',
|
|
24
|
+
description: 'Full-width hero section with headline and CTAs',
|
|
25
|
+
category: 'Hero',
|
|
26
|
+
variants: ['centered', 'split', 'gradient', 'minimal'],
|
|
27
|
+
defaultProps: { headline: 'Your Headline Here', subheadline: 'A compelling subheadline that explains your value proposition.', primaryCta: 'Get Started', secondaryCta: 'Learn More' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'features',
|
|
31
|
+
label: 'Features',
|
|
32
|
+
description: 'Feature showcase with icon cards',
|
|
33
|
+
category: 'Content',
|
|
34
|
+
variants: ['grid', 'list', 'alternating'],
|
|
35
|
+
defaultProps: { title: 'Features', subtitle: 'Everything you need', items: [{ icon: 'Zap', title: 'Fast', description: 'Lightning fast performance' }, { icon: 'Shield', title: 'Secure', description: 'Enterprise-grade security' }, { icon: 'Globe', title: 'Global', description: 'Available worldwide' }] },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'pricing',
|
|
39
|
+
label: 'Pricing',
|
|
40
|
+
description: 'Pricing tiers with feature comparison',
|
|
41
|
+
category: 'Commerce',
|
|
42
|
+
variants: ['simple', 'comparison'],
|
|
43
|
+
defaultProps: { title: 'Pricing', subtitle: 'Choose the plan that fits your needs' },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'cta',
|
|
47
|
+
label: 'Call to Action',
|
|
48
|
+
description: 'Conversion-focused section with CTA button',
|
|
49
|
+
category: 'Conversion',
|
|
50
|
+
variants: ['simple', 'split'],
|
|
51
|
+
defaultProps: { headline: 'Ready to get started?', subheadline: 'Start building today.', buttonText: 'Start Free' },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'footer',
|
|
55
|
+
label: 'Footer',
|
|
56
|
+
description: 'Page footer with links and copyright',
|
|
57
|
+
category: 'Navigation',
|
|
58
|
+
variants: ['simple', 'multi-column', 'minimal'],
|
|
59
|
+
defaultProps: { logo: 'Brand', copyright: '2026 Brand. All rights reserved.', links: ['Privacy', 'Terms'] },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'testimonials',
|
|
63
|
+
label: 'Testimonials',
|
|
64
|
+
description: 'Customer testimonials with quotes and ratings',
|
|
65
|
+
category: 'Social Proof',
|
|
66
|
+
variants: ['cards', 'carousel', 'spotlight'],
|
|
67
|
+
defaultProps: { title: 'What our customers say' },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'stats',
|
|
71
|
+
label: 'Stats',
|
|
72
|
+
description: 'Key metrics and statistics display',
|
|
73
|
+
category: 'Social Proof',
|
|
74
|
+
variants: ['grid', 'bar', 'counter'],
|
|
75
|
+
defaultProps: { title: 'By the numbers' },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'faq',
|
|
79
|
+
label: 'FAQ',
|
|
80
|
+
description: 'Frequently asked questions accordion',
|
|
81
|
+
category: 'Content',
|
|
82
|
+
variants: ['accordion'],
|
|
83
|
+
defaultProps: { title: 'Frequently Asked Questions' },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'team',
|
|
87
|
+
label: 'Team',
|
|
88
|
+
description: 'Team member grid with photos and roles',
|
|
89
|
+
category: 'Content',
|
|
90
|
+
variants: ['grid'],
|
|
91
|
+
defaultProps: { title: 'Meet the Team' },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'contact',
|
|
95
|
+
label: 'Contact',
|
|
96
|
+
description: 'Contact form with name, email, and message',
|
|
97
|
+
category: 'Forms',
|
|
98
|
+
variants: ['form'],
|
|
99
|
+
defaultProps: { title: 'Get in Touch', subtitle: "We'd love to hear from you." },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'newsletter',
|
|
103
|
+
label: 'Newsletter',
|
|
104
|
+
description: 'Email subscription form with social proof',
|
|
105
|
+
category: 'Conversion',
|
|
106
|
+
variants: ['simple'],
|
|
107
|
+
defaultProps: { title: 'Stay in the loop', subtitle: 'Get updates on new features.', buttonText: 'Subscribe' },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'logocloud',
|
|
111
|
+
label: 'Logo Cloud',
|
|
112
|
+
description: 'Company logos with hover effects',
|
|
113
|
+
category: 'Social Proof',
|
|
114
|
+
variants: ['default'],
|
|
115
|
+
defaultProps: { title: 'Trusted by leading companies' },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'content',
|
|
119
|
+
label: 'Content',
|
|
120
|
+
description: 'Rich text content section',
|
|
121
|
+
category: 'Content',
|
|
122
|
+
variants: ['prose', 'columns', 'highlight'],
|
|
123
|
+
defaultProps: { body: '## Getting Started\n\nWrite your content here. Supports **bold**, *italic*, and lists.\n\n- First item\n- Second item\n- Third item' },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'image',
|
|
127
|
+
label: 'Image',
|
|
128
|
+
description: 'Image with text overlay or side-by-side layout',
|
|
129
|
+
category: 'Media',
|
|
130
|
+
variants: ['hero-image', 'side-by-side', 'grid'],
|
|
131
|
+
defaultProps: { title: 'Visual Storytelling', subtitle: 'A picture is worth a thousand words.', imageSide: 'left' },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'video',
|
|
135
|
+
label: 'Video',
|
|
136
|
+
description: 'Embedded YouTube or Vimeo video',
|
|
137
|
+
category: 'Media',
|
|
138
|
+
variants: ['youtube', 'vimeo'],
|
|
139
|
+
defaultProps: { url: '', title: 'Watch Our Story' },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'gallery',
|
|
143
|
+
label: 'Gallery',
|
|
144
|
+
description: 'Image gallery in grid or masonry layout',
|
|
145
|
+
category: 'Media',
|
|
146
|
+
variants: ['grid', 'masonry'],
|
|
147
|
+
defaultProps: { title: 'Gallery' },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: 'divider',
|
|
151
|
+
label: 'Divider',
|
|
152
|
+
description: 'Visual separator between sections',
|
|
153
|
+
category: 'Layout',
|
|
154
|
+
variants: ['line', 'space', 'dots'],
|
|
155
|
+
defaultProps: { height: 60, width: 'full' },
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'banner',
|
|
159
|
+
label: 'Banner',
|
|
160
|
+
description: 'Announcement bar or ribbon',
|
|
161
|
+
category: 'Content',
|
|
162
|
+
variants: ['ribbon', 'bar'],
|
|
163
|
+
defaultProps: { text: 'New: We just launched v2.0!', linkText: 'Learn more' },
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
export const categories = [...new Set(blockMetadata.map((b) => b.category))]
|