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