@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,344 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
import { produce } from 'immer'
|
|
4
|
+
import type { BlockConfig, SiteConfig, ThemeConfig, PageConfig } from '@/blocks/types'
|
|
5
|
+
|
|
6
|
+
function ensurePages(config: SiteConfig): PageConfig[] {
|
|
7
|
+
if (config.pages && config.pages.length > 0) return config.pages
|
|
8
|
+
return [{ id: 'page-home', name: 'Home', path: '/', blocks: config.blocks }]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getPageBlocks(config: SiteConfig, pageId: string): BlockConfig[] {
|
|
12
|
+
const pages = ensurePages(config)
|
|
13
|
+
const page = pages.find((p) => p.id === pageId)
|
|
14
|
+
return page?.blocks ?? pages[0]?.blocks ?? []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UndoEntry {
|
|
18
|
+
pages?: PageConfig[]
|
|
19
|
+
blocks: BlockConfig[]
|
|
20
|
+
theme?: Partial<ThemeConfig>
|
|
21
|
+
label: string
|
|
22
|
+
timestamp: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ConfigState {
|
|
26
|
+
config: SiteConfig
|
|
27
|
+
activePageId: string
|
|
28
|
+
undoStack: UndoEntry[]
|
|
29
|
+
redoStack: UndoEntry[]
|
|
30
|
+
setConfig: (config: SiteConfig) => void
|
|
31
|
+
setActivePage: (id: string) => void
|
|
32
|
+
getActivePageBlocks: () => BlockConfig[]
|
|
33
|
+
updateBlock: (id: string, updates: Partial<BlockConfig>) => void
|
|
34
|
+
updateBlockProps: (id: string, props: Record<string, unknown>) => void
|
|
35
|
+
addBlock: (block: BlockConfig, index?: number) => void
|
|
36
|
+
removeBlock: (id: string) => void
|
|
37
|
+
duplicateBlock: (id: string) => void
|
|
38
|
+
moveBlock: (fromIndex: number, toIndex: number) => void
|
|
39
|
+
addPage: (name: string, path: string) => string
|
|
40
|
+
removePage: (id: string) => void
|
|
41
|
+
renamePage: (id: string, name: string) => void
|
|
42
|
+
setTheme: (theme: Partial<ThemeConfig>) => void
|
|
43
|
+
updateTheme: (partial: Partial<ThemeConfig>) => void
|
|
44
|
+
previewTheme: (partial: Partial<ThemeConfig>) => void
|
|
45
|
+
undo: () => void
|
|
46
|
+
redo: () => void
|
|
47
|
+
canUndo: () => boolean
|
|
48
|
+
canRedo: () => boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const defaultBlocks: BlockConfig[] = [
|
|
52
|
+
{
|
|
53
|
+
id: 'block-navbar',
|
|
54
|
+
type: 'navbar',
|
|
55
|
+
variant: 'default',
|
|
56
|
+
props: {
|
|
57
|
+
logo: 'Acme Inc',
|
|
58
|
+
links: ['Features', 'Pricing', 'About', 'Contact'],
|
|
59
|
+
ctaText: 'Get Started',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'block-hero',
|
|
64
|
+
type: 'hero',
|
|
65
|
+
variant: 'centered',
|
|
66
|
+
props: {
|
|
67
|
+
badge: 'Now in Beta',
|
|
68
|
+
headline: 'Build websites with JSON',
|
|
69
|
+
subheadline: 'The visual editor that agents and humans both understand. Structured config, beautiful output.',
|
|
70
|
+
primaryCta: 'Start Building',
|
|
71
|
+
secondaryCta: 'View Demo',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'block-features',
|
|
76
|
+
type: 'features',
|
|
77
|
+
variant: 'grid',
|
|
78
|
+
props: {
|
|
79
|
+
label: 'Features',
|
|
80
|
+
title: 'Everything you need',
|
|
81
|
+
subtitle: 'Powerful building blocks for your next website',
|
|
82
|
+
items: [
|
|
83
|
+
{ icon: 'Blocks', title: 'Visual Editor', description: 'Drag and drop blocks to build your layout' },
|
|
84
|
+
{ icon: 'Code', title: 'JSON Config', description: 'Every change is a clean JSON mutation' },
|
|
85
|
+
{ icon: 'Bot', title: 'Agent Ready', description: 'AI agents can read and write your config' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'block-cta',
|
|
91
|
+
type: 'cta',
|
|
92
|
+
variant: 'simple',
|
|
93
|
+
props: {
|
|
94
|
+
headline: 'Ready to get started?',
|
|
95
|
+
subheadline: 'Create your first site in minutes.',
|
|
96
|
+
buttonText: 'Start Free',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'block-footer',
|
|
101
|
+
type: 'footer',
|
|
102
|
+
variant: 'simple',
|
|
103
|
+
props: {
|
|
104
|
+
logo: 'OpenPage',
|
|
105
|
+
copyright: '2026 OpenPage. All rights reserved.',
|
|
106
|
+
links: ['Privacy', 'Terms', 'Contact'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
export const defaultConfig: SiteConfig = {
|
|
112
|
+
name: 'My Website',
|
|
113
|
+
pages: [{ id: 'page-home', name: 'Home', path: '/', blocks: defaultBlocks }],
|
|
114
|
+
blocks: defaultBlocks,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function snapshot(state: ConfigState): { pages?: PageConfig[]; blocks: BlockConfig[]; theme?: Partial<ThemeConfig> } {
|
|
118
|
+
return {
|
|
119
|
+
pages: state.config.pages ? JSON.parse(JSON.stringify(state.config.pages)) : undefined,
|
|
120
|
+
blocks: JSON.parse(JSON.stringify(state.config.blocks)),
|
|
121
|
+
theme: state.config.theme ? JSON.parse(JSON.stringify(state.config.theme)) : undefined,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const MAX_UNDO = 50
|
|
126
|
+
|
|
127
|
+
function pushUndo(state: ConfigState, label: string): Partial<ConfigState> {
|
|
128
|
+
const snap = snapshot(state)
|
|
129
|
+
return {
|
|
130
|
+
undoStack: [...state.undoStack, { ...snap, label, timestamp: Date.now() }].slice(-MAX_UNDO),
|
|
131
|
+
redoStack: [],
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function withPages(config: SiteConfig): SiteConfig {
|
|
136
|
+
const pages = ensurePages(config)
|
|
137
|
+
return { ...config, pages }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mutateActivePageBlocks(
|
|
141
|
+
config: SiteConfig,
|
|
142
|
+
activePageId: string,
|
|
143
|
+
mutator: (blocks: BlockConfig[]) => BlockConfig[],
|
|
144
|
+
): SiteConfig {
|
|
145
|
+
const pages = ensurePages(config)
|
|
146
|
+
const newPages = pages.map((p) =>
|
|
147
|
+
p.id === activePageId ? { ...p, blocks: mutator([...p.blocks]) } : p,
|
|
148
|
+
)
|
|
149
|
+
// Keep top-level blocks synced with first page for backward compat
|
|
150
|
+
const activeBlocks = newPages.find((p) => p.id === activePageId)?.blocks ?? []
|
|
151
|
+
return { ...config, pages: newPages, blocks: activeBlocks }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const useConfigStore = create<ConfigState>()(
|
|
155
|
+
persist(
|
|
156
|
+
(set, get) => ({
|
|
157
|
+
config: defaultConfig,
|
|
158
|
+
activePageId: 'page-home',
|
|
159
|
+
undoStack: [],
|
|
160
|
+
redoStack: [],
|
|
161
|
+
|
|
162
|
+
setConfig: (config) => {
|
|
163
|
+
const pages = ensurePages(config)
|
|
164
|
+
set({ config: { ...config, pages }, activePageId: pages[0]?.id ?? 'page-home', undoStack: [], redoStack: [] })
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
setActivePage: (id) => set({ activePageId: id }),
|
|
168
|
+
|
|
169
|
+
getActivePageBlocks: () => {
|
|
170
|
+
const state = get()
|
|
171
|
+
return getPageBlocks(state.config, state.activePageId)
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
updateBlock: (id, updates) =>
|
|
175
|
+
set((state) => ({
|
|
176
|
+
...pushUndo(state, 'Update block'),
|
|
177
|
+
config: produce(withPages(state.config), (draft) => {
|
|
178
|
+
const page = draft.pages!.find((p) => p.id === state.activePageId)
|
|
179
|
+
if (!page) return
|
|
180
|
+
const block = page.blocks.find((b) => b.id === id)
|
|
181
|
+
if (block) Object.assign(block, updates)
|
|
182
|
+
draft.blocks = page.blocks
|
|
183
|
+
}),
|
|
184
|
+
})),
|
|
185
|
+
|
|
186
|
+
updateBlockProps: (id, props) =>
|
|
187
|
+
set((state) => ({
|
|
188
|
+
...pushUndo(state, 'Update properties'),
|
|
189
|
+
config: produce(withPages(state.config), (draft) => {
|
|
190
|
+
const page = draft.pages!.find((p) => p.id === state.activePageId)
|
|
191
|
+
if (!page) return
|
|
192
|
+
const block = page.blocks.find((b) => b.id === id)
|
|
193
|
+
if (block) Object.assign(block.props, props)
|
|
194
|
+
draft.blocks = page.blocks
|
|
195
|
+
}),
|
|
196
|
+
})),
|
|
197
|
+
|
|
198
|
+
addBlock: (block, index) =>
|
|
199
|
+
set((state) => ({
|
|
200
|
+
...pushUndo(state, 'Add block'),
|
|
201
|
+
config: mutateActivePageBlocks(withPages(state.config), state.activePageId, (blocks) => {
|
|
202
|
+
if (index !== undefined) {
|
|
203
|
+
blocks.splice(index, 0, block)
|
|
204
|
+
} else {
|
|
205
|
+
blocks.push(block)
|
|
206
|
+
}
|
|
207
|
+
return blocks
|
|
208
|
+
}),
|
|
209
|
+
})),
|
|
210
|
+
|
|
211
|
+
removeBlock: (id) =>
|
|
212
|
+
set((state) => ({
|
|
213
|
+
...pushUndo(state, 'Remove block'),
|
|
214
|
+
config: mutateActivePageBlocks(withPages(state.config), state.activePageId, (blocks) =>
|
|
215
|
+
blocks.filter((b) => b.id !== id),
|
|
216
|
+
),
|
|
217
|
+
})),
|
|
218
|
+
|
|
219
|
+
duplicateBlock: (id) =>
|
|
220
|
+
set((state) => {
|
|
221
|
+
const blocks = getPageBlocks(state.config, state.activePageId)
|
|
222
|
+
const idx = blocks.findIndex((b) => b.id === id)
|
|
223
|
+
if (idx === -1) return state
|
|
224
|
+
const original = blocks[idx]
|
|
225
|
+
const clone: BlockConfig = {
|
|
226
|
+
...JSON.parse(JSON.stringify(original)),
|
|
227
|
+
id: `block-${Date.now()}`,
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...pushUndo(state, 'Duplicate block'),
|
|
231
|
+
config: mutateActivePageBlocks(withPages(state.config), state.activePageId, (b) => {
|
|
232
|
+
b.splice(idx + 1, 0, clone)
|
|
233
|
+
return b
|
|
234
|
+
}),
|
|
235
|
+
}
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
moveBlock: (fromIndex, toIndex) =>
|
|
239
|
+
set((state) => ({
|
|
240
|
+
...pushUndo(state, 'Move block'),
|
|
241
|
+
config: mutateActivePageBlocks(withPages(state.config), state.activePageId, (blocks) => {
|
|
242
|
+
const [moved] = blocks.splice(fromIndex, 1)
|
|
243
|
+
blocks.splice(toIndex, 0, moved)
|
|
244
|
+
return blocks
|
|
245
|
+
}),
|
|
246
|
+
})),
|
|
247
|
+
|
|
248
|
+
addPage: (name, path) => {
|
|
249
|
+
const id = `page-${Date.now()}`
|
|
250
|
+
set((state) => ({
|
|
251
|
+
...pushUndo(state, 'Add page'),
|
|
252
|
+
config: produce(withPages(state.config), (draft) => {
|
|
253
|
+
draft.pages!.push({ id, name, path, blocks: [] })
|
|
254
|
+
}),
|
|
255
|
+
activePageId: id,
|
|
256
|
+
}))
|
|
257
|
+
return id
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
removePage: (id) =>
|
|
261
|
+
set((state) => {
|
|
262
|
+
const pages = ensurePages(state.config)
|
|
263
|
+
if (pages.length <= 1) return state
|
|
264
|
+
const newPages = pages.filter((p) => p.id !== id)
|
|
265
|
+
const newActiveId = state.activePageId === id ? newPages[0].id : state.activePageId
|
|
266
|
+
return {
|
|
267
|
+
...pushUndo(state, 'Remove page'),
|
|
268
|
+
config: { ...state.config, pages: newPages, blocks: newPages[0].blocks },
|
|
269
|
+
activePageId: newActiveId,
|
|
270
|
+
}
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
renamePage: (id, name) =>
|
|
274
|
+
set((state) => ({
|
|
275
|
+
config: produce(withPages(state.config), (draft) => {
|
|
276
|
+
const page = draft.pages!.find((p) => p.id === id)
|
|
277
|
+
if (page) page.name = name
|
|
278
|
+
}),
|
|
279
|
+
})),
|
|
280
|
+
|
|
281
|
+
setTheme: (theme) =>
|
|
282
|
+
set((state) => ({
|
|
283
|
+
...pushUndo(state, 'Change theme'),
|
|
284
|
+
config: { ...state.config, theme },
|
|
285
|
+
})),
|
|
286
|
+
|
|
287
|
+
updateTheme: (partial) =>
|
|
288
|
+
set((state) => ({
|
|
289
|
+
...pushUndo(state, 'Update theme'),
|
|
290
|
+
config: { ...state.config, theme: { ...state.config.theme, ...partial } },
|
|
291
|
+
})),
|
|
292
|
+
|
|
293
|
+
previewTheme: (partial) =>
|
|
294
|
+
set((state) => ({
|
|
295
|
+
config: { ...state.config, theme: { ...state.config.theme, ...partial } },
|
|
296
|
+
})),
|
|
297
|
+
|
|
298
|
+
undo: () =>
|
|
299
|
+
set((state) => {
|
|
300
|
+
if (state.undoStack.length === 0) return state
|
|
301
|
+
const prev = state.undoStack[state.undoStack.length - 1]
|
|
302
|
+
const snap = snapshot(state)
|
|
303
|
+
return {
|
|
304
|
+
undoStack: state.undoStack.slice(0, -1),
|
|
305
|
+
redoStack: [...state.redoStack, { ...snap, label: prev.label, timestamp: Date.now() }],
|
|
306
|
+
config: { ...state.config, pages: prev.pages, blocks: prev.blocks, theme: prev.theme },
|
|
307
|
+
}
|
|
308
|
+
}),
|
|
309
|
+
|
|
310
|
+
redo: () =>
|
|
311
|
+
set((state) => {
|
|
312
|
+
if (state.redoStack.length === 0) return state
|
|
313
|
+
const next = state.redoStack[state.redoStack.length - 1]
|
|
314
|
+
const snap = snapshot(state)
|
|
315
|
+
return {
|
|
316
|
+
redoStack: state.redoStack.slice(0, -1),
|
|
317
|
+
undoStack: [...state.undoStack, { ...snap, label: next.label, timestamp: Date.now() }],
|
|
318
|
+
config: { ...state.config, pages: next.pages, blocks: next.blocks, theme: next.theme },
|
|
319
|
+
}
|
|
320
|
+
}),
|
|
321
|
+
|
|
322
|
+
canUndo: () => get().undoStack.length > 0,
|
|
323
|
+
canRedo: () => get().redoStack.length > 0,
|
|
324
|
+
}),
|
|
325
|
+
{
|
|
326
|
+
name: 'openpage-config',
|
|
327
|
+
version: 2,
|
|
328
|
+
partialize: (state) => ({ config: state.config, activePageId: state.activePageId }),
|
|
329
|
+
migrate: (persisted, version) => {
|
|
330
|
+
const data = persisted as Record<string, unknown>
|
|
331
|
+
if (version === 0 || version === 1 || version === undefined) {
|
|
332
|
+
// v0/v1 -> v2: wrap blocks[] into pages[]
|
|
333
|
+
const config = data.config as SiteConfig | undefined
|
|
334
|
+
if (config && !config.pages) {
|
|
335
|
+
config.pages = [{ id: 'page-home', name: 'Home', path: '/', blocks: config.blocks || [] }]
|
|
336
|
+
}
|
|
337
|
+
data.activePageId = 'page-home'
|
|
338
|
+
return data
|
|
339
|
+
}
|
|
340
|
+
return data
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
export type Viewport = 'desktop' | 'tablet' | 'mobile'
|
|
4
|
+
|
|
5
|
+
interface EditorState {
|
|
6
|
+
selectedBlockId: string | null
|
|
7
|
+
viewport: Viewport
|
|
8
|
+
jsonDrawerOpen: boolean
|
|
9
|
+
historyOpen: boolean
|
|
10
|
+
shortcutsModalOpen: boolean
|
|
11
|
+
previewMode: boolean
|
|
12
|
+
activeProjectId: string | null
|
|
13
|
+
// Generation state
|
|
14
|
+
isGenerating: boolean
|
|
15
|
+
generationPrompt: string | null
|
|
16
|
+
generationError: string | null
|
|
17
|
+
selectBlock: (id: string | null) => void
|
|
18
|
+
setViewport: (vp: Viewport) => void
|
|
19
|
+
toggleJsonDrawer: () => void
|
|
20
|
+
toggleHistory: () => void
|
|
21
|
+
toggleShortcutsModal: () => void
|
|
22
|
+
togglePreview: () => void
|
|
23
|
+
setActiveProject: (id: string | null) => void
|
|
24
|
+
setGenerating: (prompt: string | null) => void
|
|
25
|
+
setGenerationError: (err: string | null) => void
|
|
26
|
+
clearGeneration: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const useEditorStore = create<EditorState>()((set) => ({
|
|
30
|
+
selectedBlockId: null,
|
|
31
|
+
viewport: 'desktop',
|
|
32
|
+
jsonDrawerOpen: false,
|
|
33
|
+
historyOpen: false,
|
|
34
|
+
shortcutsModalOpen: false,
|
|
35
|
+
previewMode: false,
|
|
36
|
+
activeProjectId: null,
|
|
37
|
+
isGenerating: false,
|
|
38
|
+
generationPrompt: null,
|
|
39
|
+
generationError: null,
|
|
40
|
+
selectBlock: (id) => set({ selectedBlockId: id }),
|
|
41
|
+
setViewport: (vp) => set({ viewport: vp }),
|
|
42
|
+
toggleJsonDrawer: () => set((s) => ({ jsonDrawerOpen: !s.jsonDrawerOpen })),
|
|
43
|
+
toggleHistory: () => set((s) => ({ historyOpen: !s.historyOpen })),
|
|
44
|
+
toggleShortcutsModal: () => set((s) => ({ shortcutsModalOpen: !s.shortcutsModalOpen })),
|
|
45
|
+
togglePreview: () => set((s) => ({ previewMode: !s.previewMode, ...(!s.previewMode ? { selectedBlockId: null } : {}) })),
|
|
46
|
+
setActiveProject: (id) => set({ activeProjectId: id }),
|
|
47
|
+
setGenerating: (prompt) => set({ isGenerating: !!prompt, generationPrompt: prompt, generationError: null }),
|
|
48
|
+
setGenerationError: (err) => set({ generationError: err }),
|
|
49
|
+
clearGeneration: () => set({ isGenerating: false, generationPrompt: null }),
|
|
50
|
+
}))
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @siteable/core — Tailwind v4 source stylesheet (raw, unbuilt).
|
|
3
|
+
*
|
|
4
|
+
* Ships the engine-side design tokens, animations, and utilities. Consumers MUST
|
|
5
|
+
* `@import "..."` this file from their own global stylesheet (after their own
|
|
6
|
+
* `@import "tailwindcss"`) and add `@source` entries pointing at the package's
|
|
7
|
+
* src/ directory so Tailwind v4's scanner picks up the block component classes.
|
|
8
|
+
*
|
|
9
|
+
* EXCLUDED (page-level resets, app-side concerns): `@import "tailwindcss"`,
|
|
10
|
+
* `:root { --color-accent-rgb }`, `html { font-size: 14px }`, body/scrollbar/
|
|
11
|
+
* focus-visible resets — these belong to the host app's globals.
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: accent-rgb fallbacks (`var(--color-accent-rgb, 34, 197, 94)`)
|
|
14
|
+
* retained verbatim — host apps that omit the `:root` override still get the
|
|
15
|
+
* default Siteable green via the fallback channel.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
@theme {
|
|
19
|
+
/* Background layers */
|
|
20
|
+
--color-bg-0: #09090b;
|
|
21
|
+
--color-bg-1: #0f0f12;
|
|
22
|
+
--color-bg-2: #18181b;
|
|
23
|
+
--color-bg-3: #1e1e23;
|
|
24
|
+
--color-bg-4: #27272a;
|
|
25
|
+
--color-bg-5: #303036;
|
|
26
|
+
|
|
27
|
+
/* Borders */
|
|
28
|
+
--color-border-default: #2a2a2f;
|
|
29
|
+
--color-border-subtle: #1f1f24;
|
|
30
|
+
--color-border-hover: #3a3a3f;
|
|
31
|
+
|
|
32
|
+
/* Text */
|
|
33
|
+
--color-text-0: #fafafa;
|
|
34
|
+
--color-text-1: #a1a1aa;
|
|
35
|
+
--color-text-2: #71717a;
|
|
36
|
+
--color-text-3: #52525b;
|
|
37
|
+
|
|
38
|
+
/* Accent */
|
|
39
|
+
--color-green: #22c55e;
|
|
40
|
+
--color-green-dim: #16a34a;
|
|
41
|
+
--color-green-glow: rgba(34, 197, 94, 0.12);
|
|
42
|
+
--color-green-glow2: rgba(34, 197, 94, 0.06);
|
|
43
|
+
|
|
44
|
+
/* Status */
|
|
45
|
+
--color-status-red: #ef4444;
|
|
46
|
+
--color-status-yellow: #eab308;
|
|
47
|
+
--color-status-blue: #3b82f6;
|
|
48
|
+
|
|
49
|
+
/* Radius */
|
|
50
|
+
--radius-default: 8px;
|
|
51
|
+
--radius-lg: 12px;
|
|
52
|
+
|
|
53
|
+
/* Fonts */
|
|
54
|
+
--font-sans: "DM Sans", -apple-system, system-ui, sans-serif;
|
|
55
|
+
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
|
56
|
+
--font-display: "Space Grotesk", system-ui, sans-serif;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Page-level stagger reveal */
|
|
60
|
+
@keyframes fadeInUp {
|
|
61
|
+
from {
|
|
62
|
+
opacity: 0;
|
|
63
|
+
transform: translateY(12px);
|
|
64
|
+
}
|
|
65
|
+
to {
|
|
66
|
+
opacity: 1;
|
|
67
|
+
transform: translateY(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes fadeIn {
|
|
72
|
+
from { opacity: 0; }
|
|
73
|
+
to { opacity: 1; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@keyframes scaleIn {
|
|
77
|
+
from {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transform: scale(0.96);
|
|
80
|
+
}
|
|
81
|
+
to {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
transform: scale(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes shimmer {
|
|
88
|
+
0% { background-position: -200% 0; }
|
|
89
|
+
100% { background-position: 200% 0; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes selectPulse {
|
|
93
|
+
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.3); }
|
|
94
|
+
50% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }
|
|
95
|
+
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@utility animate-fade-in-up {
|
|
99
|
+
animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@utility animate-fade-in {
|
|
103
|
+
animation: fadeIn 0.3s ease-out both;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@utility animate-scale-in {
|
|
107
|
+
animation: scaleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@utility animate-shimmer {
|
|
111
|
+
animation: shimmer 1.5s ease-in-out infinite;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@utility animate-select-pulse {
|
|
115
|
+
animation: selectPulse 0.4s ease-out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@utility stagger-1 { animation-delay: 0ms; }
|
|
119
|
+
@utility stagger-2 { animation-delay: 50ms; }
|
|
120
|
+
@utility stagger-3 { animation-delay: 100ms; }
|
|
121
|
+
@utility stagger-4 { animation-delay: 150ms; }
|
|
122
|
+
@utility stagger-5 { animation-delay: 200ms; }
|
|
123
|
+
@utility stagger-6 { animation-delay: 250ms; }
|
|
124
|
+
@utility stagger-7 { animation-delay: 300ms; }
|
|
125
|
+
@utility stagger-8 { animation-delay: 350ms; }
|
|
126
|
+
|
|
127
|
+
/* Card hover lift */
|
|
128
|
+
@utility card-lift {
|
|
129
|
+
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.2s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@utility card-lift-hover {
|
|
133
|
+
transform: translateY(-4px);
|
|
134
|
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35), 0 4px 10px rgba(0, 0, 0, 0.2), 0 0 16px rgba(34, 197, 94, 0.06);
|
|
135
|
+
border-color: var(--color-border-hover);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Accent glow shadows - @utility so Tailwind v4 generates hover:/group-hover: variants */
|
|
139
|
+
@utility accent-glow-sm {
|
|
140
|
+
box-shadow: 0 0 16px rgba(var(--color-accent-rgb, 34, 197, 94), 0.1);
|
|
141
|
+
}
|
|
142
|
+
@utility accent-glow-md {
|
|
143
|
+
box-shadow: 0 0 16px rgba(var(--color-accent-rgb, 34, 197, 94), 0.15);
|
|
144
|
+
}
|
|
145
|
+
@utility accent-glow-lg {
|
|
146
|
+
box-shadow: 0 0 20px rgba(var(--color-accent-rgb, 34, 197, 94), 0.25);
|
|
147
|
+
}
|
|
148
|
+
@utility accent-glow-xl {
|
|
149
|
+
box-shadow: 0 0 24px rgba(var(--color-accent-rgb, 34, 197, 94), 0.3);
|
|
150
|
+
}
|
|
151
|
+
@utility accent-glow-ring {
|
|
152
|
+
box-shadow: 0 0 32px rgba(var(--color-accent-rgb, 34, 197, 94), 0.1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Scroll-reveal animation system
|
|
156
|
+
Parent gets .scroll-revealed when visible in viewport.
|
|
157
|
+
Children with .reveal-* classes animate in with staggered delays. */
|
|
158
|
+
|
|
159
|
+
.reveal-fade-up,
|
|
160
|
+
.reveal-scale,
|
|
161
|
+
.reveal-slide-left,
|
|
162
|
+
.reveal-slide-right {
|
|
163
|
+
opacity: 0;
|
|
164
|
+
transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
|
|
165
|
+
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
|
166
|
+
transition-delay: var(--reveal-delay, 0ms);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.reveal-fade-up { transform: translateY(16px); }
|
|
170
|
+
.reveal-scale { transform: scale(0.95); }
|
|
171
|
+
.reveal-slide-left { transform: translateX(-20px); }
|
|
172
|
+
.reveal-slide-right { transform: translateX(20px); }
|
|
173
|
+
|
|
174
|
+
.scroll-revealed .reveal-fade-up,
|
|
175
|
+
.scroll-revealed .reveal-scale,
|
|
176
|
+
.scroll-revealed .reveal-slide-left,
|
|
177
|
+
.scroll-revealed .reveal-slide-right {
|
|
178
|
+
opacity: 1;
|
|
179
|
+
transform: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Stagger delay utilities */
|
|
183
|
+
@utility reveal-d1 { --reveal-delay: 0ms; }
|
|
184
|
+
@utility reveal-d2 { --reveal-delay: 80ms; }
|
|
185
|
+
@utility reveal-d3 { --reveal-delay: 160ms; }
|
|
186
|
+
@utility reveal-d4 { --reveal-delay: 240ms; }
|
|
187
|
+
@utility reveal-d5 { --reveal-delay: 320ms; }
|
|
188
|
+
@utility reveal-d6 { --reveal-delay: 400ms; }
|
|
189
|
+
@utility reveal-d7 { --reveal-delay: 480ms; }
|
|
190
|
+
@utility reveal-d8 { --reveal-delay: 560ms; }
|
|
191
|
+
|
|
192
|
+
/* Skip to content link - visible only on focus */
|
|
193
|
+
.skip-to-content {
|
|
194
|
+
position: fixed;
|
|
195
|
+
top: -100%;
|
|
196
|
+
left: 50%;
|
|
197
|
+
transform: translateX(-50%);
|
|
198
|
+
z-index: 200;
|
|
199
|
+
padding: 8px 16px;
|
|
200
|
+
background: var(--color-green);
|
|
201
|
+
color: #000;
|
|
202
|
+
font-size: 13px;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
border-radius: 0 0 8px 8px;
|
|
205
|
+
transition: top 0.2s;
|
|
206
|
+
}
|
|
207
|
+
.skip-to-content:focus {
|
|
208
|
+
top: 0;
|
|
209
|
+
outline: none;
|
|
210
|
+
}
|