@vibecms/cli 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/bin/vibe.js +497 -0
- package/package.json +39 -0
- package/templates/components/cms/Analytics.tsx +42 -0
- package/templates/components/cms/BlockRenderer.tsx +37 -0
- package/templates/components/cms/CmsStage.tsx +116 -0
- package/templates/components/cms/EditProvider.tsx +121 -0
- package/templates/components/cms/Editor.tsx +954 -0
- package/templates/components/cms/FormGenerator.tsx +611 -0
- package/templates/components/cms/SEO.tsx +35 -0
- package/templates/components/cms/SiteFooter.tsx +39 -0
- package/templates/components/cms/SiteHeader.tsx +43 -0
- package/templates/components/cms/VisualWrapper.tsx +128 -0
- package/templates/components/cms/fields/ColorPicker.tsx +71 -0
- package/templates/components/cms/fields/IconPicker.tsx +67 -0
- package/templates/components/cms/fields/ImageUpload.tsx +120 -0
- package/templates/components/cms/fields/MediaGallery.tsx +176 -0
- package/templates/components/cms/fields/MultiReferencePicker.tsx +83 -0
- package/templates/components/cms/fields/ReferencePicker.tsx +75 -0
- package/templates/components/cms/fields/RichText.tsx +121 -0
- package/templates/lib/cms/auditor.ts +307 -0
- package/templates/lib/cms/auth-nextauth.ts +26 -0
- package/templates/lib/cms/engine.ts +3 -0
- package/templates/lib/cms/registry.ts +12 -0
- package/templates/lib/cms/sanitize.ts +18 -0
- package/templates/lib/cms/schema.ts +51 -0
- package/templates/lib/cms/store.ts +59 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import { isEditMode, previewDevice, setFocusedField } from '@/lib/cms/store';
|
|
5
|
+
import { motion } from 'framer-motion';
|
|
6
|
+
import { Monitor, Smartphone, Tablet } from 'lucide-react';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
|
|
9
|
+
export function CmsStage({ children, bypass = false }: { children: React.ReactNode; bypass?: boolean }) {
|
|
10
|
+
const editing = useStore(isEditMode);
|
|
11
|
+
const device = useStore(previewDevice);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!editing || typeof PerformanceObserver === 'undefined') return;
|
|
15
|
+
try {
|
|
16
|
+
const observer = new PerformanceObserver((list) => {
|
|
17
|
+
for (const entry of list.getEntries() as any[]) {
|
|
18
|
+
// If the shift magnitude crosses 0.1, it's a severe Core Web Vitals fracture.
|
|
19
|
+
if (entry.value > 0.1 && !entry.hadRecentInput) {
|
|
20
|
+
toast.warning('Layout Shift Spike Detected!', {
|
|
21
|
+
description: `This change caused a massive Cumulative Layout Shift (CLS: ${entry.value.toFixed(2)}). It may significantly break mobile wrapping or push critical elements off-screen.`,
|
|
22
|
+
id: 'cls-spike',
|
|
23
|
+
duration: 6000
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
observer.observe({ type: 'layout-shift', buffered: false });
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
} catch(e) {
|
|
31
|
+
// Safely ignore if the browser environments lack layout-shift metrics capability
|
|
32
|
+
}
|
|
33
|
+
}, [editing]);
|
|
34
|
+
|
|
35
|
+
if (bypass || !editing) return <>{children}</>;
|
|
36
|
+
|
|
37
|
+
const getWidth = () => {
|
|
38
|
+
switch (device) {
|
|
39
|
+
case 'mobile': return 375;
|
|
40
|
+
case 'tablet': return 768;
|
|
41
|
+
case 'desktop': return '100%';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex-1 flex flex-col h-screen overflow-hidden bg-neutral-100 relative">
|
|
47
|
+
{/* Dot Grid Background */}
|
|
48
|
+
<div
|
|
49
|
+
className="absolute inset-0 pointer-events-none opacity-[0.03]"
|
|
50
|
+
style={{
|
|
51
|
+
backgroundImage: 'radial-gradient(circle at center, #000000 1px, transparent 1px)',
|
|
52
|
+
backgroundSize: '24px 24px'
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Top Bar Navigation */}
|
|
57
|
+
<div className="h-14 flex items-center justify-center gap-2 border-b border-neutral-200 bg-white/80 backdrop-blur-xl z-20 px-4 shrink-0 shadow-sm">
|
|
58
|
+
<div className="flex bg-neutral-50 p-1 rounded-lg border border-neutral-200 shadow-inner">
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => previewDevice.set('desktop')}
|
|
61
|
+
className={`p-1.5 rounded-md transition-all ${device === 'desktop' ? 'bg-white text-neutral-900 shadow-sm border border-neutral-200/50' : 'text-neutral-400 hover:text-neutral-900 hover:bg-neutral-100'}`}
|
|
62
|
+
title="Desktop View"
|
|
63
|
+
>
|
|
64
|
+
<Monitor className="w-4 h-4" />
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => previewDevice.set('tablet')}
|
|
68
|
+
className={`p-1.5 rounded-md transition-all ${device === 'tablet' ? 'bg-white text-neutral-900 shadow-sm border border-neutral-200/50' : 'text-neutral-400 hover:text-neutral-900 hover:bg-neutral-100'}`}
|
|
69
|
+
title="Tablet View"
|
|
70
|
+
>
|
|
71
|
+
<Tablet className="w-4 h-4" />
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => previewDevice.set('mobile')}
|
|
75
|
+
className={`p-1.5 rounded-md transition-all ${device === 'mobile' ? 'bg-white text-neutral-900 shadow-sm border border-neutral-200/50' : 'text-neutral-400 hover:text-neutral-900 hover:bg-neutral-100'}`}
|
|
76
|
+
title="Mobile View"
|
|
77
|
+
>
|
|
78
|
+
<Smartphone className="w-4 h-4" />
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Stage Canvas */}
|
|
84
|
+
<div
|
|
85
|
+
className="flex-1 flex items-center justify-center overflow-hidden p-4 relative z-10 min-h-0"
|
|
86
|
+
onClickCapture={(e) => {
|
|
87
|
+
if (!editing) return;
|
|
88
|
+
const target = e.target as HTMLElement;
|
|
89
|
+
const vibeEl = target.closest('[data-vibe-path]') as HTMLElement;
|
|
90
|
+
if (vibeEl && vibeEl.dataset.vibePath) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
setFocusedField(vibeEl.dataset.vibePath, true);
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<motion.div
|
|
98
|
+
layout
|
|
99
|
+
initial={false}
|
|
100
|
+
animate={{
|
|
101
|
+
width: getWidth(),
|
|
102
|
+
scale: 0.9,
|
|
103
|
+
}}
|
|
104
|
+
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
105
|
+
className="h-full bg-white rounded-xl shadow-2xl overflow-hidden ring-1 ring-neutral-200 relative flex flex-col"
|
|
106
|
+
style={{ transformOrigin: 'center center' }}
|
|
107
|
+
>
|
|
108
|
+
{/* Simulate a browser shell for the viewport so scrolling is contained */}
|
|
109
|
+
<div id="vibe-preview-wrapper" className="flex-1 overflow-y-auto no-scrollbar relative w-full h-full bg-white">
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
</motion.div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/components/cms/EditProvider.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useStore } from '@nanostores/react';
|
|
5
|
+
import { AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { isEditMode, contentDraft, currentDocument } from '@/lib/cms/store';
|
|
7
|
+
import { Editor, EDITOR_TOTAL_WIDTH } from '@/components/cms/Editor';
|
|
8
|
+
import { CmsStage } from '@/components/cms/CmsStage';
|
|
9
|
+
import { VibeEngine } from '@/lib/cms/engine';
|
|
10
|
+
import { TooltipProvider } from '@vibecms/core';
|
|
11
|
+
// @ts-ignore - Expected error during VibeCMS CLI development because the config lives in the user project
|
|
12
|
+
import vibeConfig from '../../../vibe.config';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param mode - 'canvas' (default): Full split-view editor with device preview via CmsStage.
|
|
16
|
+
* 'docked': DevTools-style docked sidebar. Page content reflows via html margin.
|
|
17
|
+
* Use 'docked' for existing websites that don't want their layout restructured.
|
|
18
|
+
*/
|
|
19
|
+
export function EditProvider({ children, mode = 'canvas' }: { children: React.ReactNode; mode?: 'canvas' | 'docked' }) {
|
|
20
|
+
const editing = useStore(isEditMode);
|
|
21
|
+
const auth = vibeConfig.auth;
|
|
22
|
+
const [session, setSession] = useState<{ user: any } | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Init Engine from Vibe config
|
|
26
|
+
if (vibeConfig.contentDir || vibeConfig.publicDir) {
|
|
27
|
+
VibeEngine.init({ contentDir: vibeConfig.contentDir, publicDir: vibeConfig.publicDir });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (auth) {
|
|
31
|
+
auth.getSession().then((s: any) => setSession(s));
|
|
32
|
+
}
|
|
33
|
+
}, [auth]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (auth && !session) {
|
|
37
|
+
isEditMode.set(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
42
|
+
if (urlParams.get('edit') === 'true' || localStorage.getItem('vibe_edit') === 'true') {
|
|
43
|
+
isEditMode.set(true);
|
|
44
|
+
localStorage.setItem('vibe_edit', 'true');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const unlisten = (e: KeyboardEvent) => {
|
|
48
|
+
if (e.key === 'e' && (e.metaKey || e.ctrlKey)) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const current = isEditMode.get();
|
|
51
|
+
isEditMode.set(!current);
|
|
52
|
+
localStorage.setItem('vibe_edit', (!current).toString());
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener('keydown', unlisten);
|
|
56
|
+
return () => document.removeEventListener('keydown', unlisten);
|
|
57
|
+
}, [session, auth]);
|
|
58
|
+
|
|
59
|
+
// ─── Docked mode: body-margin management ─────────────────────────────
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (mode !== 'docked') return;
|
|
62
|
+
|
|
63
|
+
const html = document.documentElement;
|
|
64
|
+
const isMobileView = window.innerWidth < 768;
|
|
65
|
+
|
|
66
|
+
if (editing && !isMobileView) {
|
|
67
|
+
html.style.transition = 'margin 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
68
|
+
html.style.marginRight = `${EDITOR_TOTAL_WIDTH}px`;
|
|
69
|
+
html.style.overflowX = 'hidden';
|
|
70
|
+
} else {
|
|
71
|
+
html.style.transition = 'margin 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
72
|
+
html.style.marginRight = '';
|
|
73
|
+
html.style.overflowX = '';
|
|
74
|
+
const tid = setTimeout(() => { html.style.transition = ''; }, 350);
|
|
75
|
+
return () => clearTimeout(tid);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onResize = () => {
|
|
79
|
+
if (!editing) return;
|
|
80
|
+
const mobile = window.innerWidth < 768;
|
|
81
|
+
if (mobile) {
|
|
82
|
+
html.style.marginRight = '';
|
|
83
|
+
} else {
|
|
84
|
+
html.style.marginRight = `${EDITOR_TOTAL_WIDTH}px`;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
window.addEventListener('resize', onResize);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener('resize', onResize);
|
|
91
|
+
html.style.marginRight = '';
|
|
92
|
+
html.style.overflowX = '';
|
|
93
|
+
html.style.transition = '';
|
|
94
|
+
};
|
|
95
|
+
}, [editing, mode]);
|
|
96
|
+
|
|
97
|
+
if (mode === 'docked') {
|
|
98
|
+
return (
|
|
99
|
+
<TooltipProvider>
|
|
100
|
+
{children}
|
|
101
|
+
<AnimatePresence>
|
|
102
|
+
{editing && <Editor auth={auth} />}
|
|
103
|
+
</AnimatePresence>
|
|
104
|
+
</TooltipProvider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Canvas mode (default): existing split-view behavior ────────────
|
|
109
|
+
return (
|
|
110
|
+
<TooltipProvider>
|
|
111
|
+
<div className={editing ? "flex h-screen w-full overflow-hidden bg-neutral-100 text-neutral-900" : "relative h-full w-full"}>
|
|
112
|
+
<CmsStage>
|
|
113
|
+
{children}
|
|
114
|
+
</CmsStage>
|
|
115
|
+
<AnimatePresence>
|
|
116
|
+
{editing && <Editor auth={auth} />}
|
|
117
|
+
</AnimatePresence>
|
|
118
|
+
</div>
|
|
119
|
+
</TooltipProvider>
|
|
120
|
+
);
|
|
121
|
+
}
|