Skip to content

Commit 4b11b0e

Browse files
committed
feat: enhance interactive styles and CSS generation in builder
- Introduced interactive state styles (hover and focus) for nodes in the AST. - Added utility functions to handle interactive styles and generate CSS blocks. - Updated the CSS generation logic to include effective styles from nodes. - Refactored the rendering logic to accommodate new interactive styles. - Created a new module for edit support, encapsulating various helper functions. - Improved the BuilderEdit component by integrating the new edit support functions and streamlining the code. - Replaced legacy code with new components for better maintainability and readability.
1 parent 7086ea3 commit 4b11b0e

File tree

11 files changed

+1811
-1193
lines changed

11 files changed

+1811
-1193
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import {
2+
BracesIcon,
3+
CodeXmlIcon,
4+
ExpandIcon,
5+
Minimize2Icon,
6+
PaintbrushIcon,
7+
XIcon,
8+
} from 'lucide-react';
9+
import type { RefObject } from 'react';
10+
import { MonacoEditor } from '@/components/code-editor/monaco-editor';
11+
import { Button } from '@/components/ui/button';
12+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
13+
import { BuilderPreviewPanel } from './builder-preview-panel';
14+
import type { BuilderDeviceMode } from './builder-utils';
15+
16+
type FooterEditorTab = 'html' | 'css' | 'js';
17+
18+
type BuilderCenterWorkspaceProps = {
19+
deviceMode: BuilderDeviceMode;
20+
iframeRef: RefObject<HTMLIFrameElement | null>;
21+
overlayContainerRef: RefObject<HTMLDivElement | null>;
22+
previewUrl: string | null;
23+
previewDocument: string;
24+
pageTitle: string;
25+
footerEditorOpen: boolean;
26+
footerEditorFullscreen: boolean;
27+
footerEditorTab: FooterEditorTab;
28+
footerEditorTitle: string;
29+
footerEditorLanguage: 'html' | 'css' | 'js';
30+
footerEditorValue: string;
31+
footerEditorIsDirty: boolean;
32+
onPreviewLoad: () => void;
33+
onOpenFooterEditor: (tab: FooterEditorTab) => void;
34+
onCloseFooterEditor: () => void;
35+
onToggleFooterEditorFullscreen: () => void;
36+
onFooterEditorValueChange: (value: string) => void;
37+
onApplyFooterEditor: () => void;
38+
};
39+
40+
export function BuilderCenterWorkspace({
41+
deviceMode,
42+
iframeRef,
43+
overlayContainerRef,
44+
previewUrl,
45+
previewDocument,
46+
pageTitle,
47+
footerEditorOpen,
48+
footerEditorFullscreen,
49+
footerEditorTab,
50+
footerEditorTitle,
51+
footerEditorLanguage,
52+
footerEditorValue,
53+
footerEditorIsDirty,
54+
onPreviewLoad,
55+
onOpenFooterEditor,
56+
onCloseFooterEditor,
57+
onToggleFooterEditorFullscreen,
58+
onFooterEditorValueChange,
59+
onApplyFooterEditor,
60+
}: BuilderCenterWorkspaceProps) {
61+
return (
62+
<div className="flex h-full min-w-0 flex-col">
63+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden border border-border/60 border-b-0 bg-background">
64+
{footerEditorOpen && footerEditorFullscreen ? (
65+
<div className="flex min-h-0 flex-1 flex-col">
66+
<div className="flex items-center justify-between border-b border-border/60 px-2 py-1">
67+
<div className="flex items-center gap-1">
68+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Editor</span>
69+
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-foreground/80">
70+
{footerEditorTitle}
71+
</span>
72+
</div>
73+
<div className="flex items-center gap-1">
74+
<Button
75+
variant="outline"
76+
size="sm"
77+
onClick={onApplyFooterEditor}
78+
disabled={!footerEditorIsDirty}
79+
className="h-6 px-2.5 text-[11px]"
80+
>
81+
Apply
82+
</Button>
83+
<Button
84+
variant="ghost"
85+
size="icon-sm"
86+
onClick={onToggleFooterEditorFullscreen}
87+
aria-label="Exit fullscreen editor"
88+
>
89+
<Minimize2Icon className="size-3.5" />
90+
</Button>
91+
<Button
92+
variant="ghost"
93+
size="icon-sm"
94+
onClick={onCloseFooterEditor}
95+
aria-label="Close editor"
96+
>
97+
<XIcon className="size-3.5" />
98+
</Button>
99+
</div>
100+
</div>
101+
<div className="min-h-0 flex-1" data-builder-shortcut-scope="footer-code">
102+
<MonacoEditor
103+
value={footerEditorValue}
104+
onChange={onFooterEditorValueChange}
105+
language={footerEditorLanguage}
106+
height="100%"
107+
/>
108+
</div>
109+
</div>
110+
) : (
111+
<>
112+
{footerEditorOpen ? (
113+
<ResizablePanelGroup orientation="vertical" className="min-h-0 flex-1">
114+
<ResizablePanel defaultSize="58%" minSize="25%">
115+
<PreviewSurface
116+
deviceMode={deviceMode}
117+
iframeRef={iframeRef}
118+
overlayContainerRef={overlayContainerRef}
119+
previewUrl={previewUrl}
120+
previewDocument={previewDocument}
121+
pageTitle={pageTitle}
122+
onPreviewLoad={onPreviewLoad}
123+
/>
124+
</ResizablePanel>
125+
<ResizableHandle withHandle />
126+
<ResizablePanel defaultSize="42%" minSize="18%">
127+
<div className="flex h-full min-h-0 flex-col border-t border-border/60">
128+
<div className="flex items-center justify-between border-b border-border/60 px-2 py-1">
129+
<div className="flex items-center gap-1">
130+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Editor</span>
131+
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-foreground/80">
132+
{footerEditorTitle}
133+
</span>
134+
</div>
135+
<div className="flex items-center gap-1">
136+
<Button
137+
variant="outline"
138+
size="sm"
139+
onClick={onApplyFooterEditor}
140+
disabled={!footerEditorIsDirty}
141+
className="h-6 px-2.5 text-[11px]"
142+
>
143+
Apply
144+
</Button>
145+
<Button
146+
variant="ghost"
147+
size="icon-sm"
148+
onClick={onToggleFooterEditorFullscreen}
149+
aria-label="Expand editor"
150+
>
151+
<ExpandIcon className="size-3.5" />
152+
</Button>
153+
<Button
154+
variant="ghost"
155+
size="icon-sm"
156+
onClick={onCloseFooterEditor}
157+
aria-label="Close editor"
158+
>
159+
<XIcon className="size-3.5" />
160+
</Button>
161+
</div>
162+
</div>
163+
<div className="min-h-0 flex-1" data-builder-shortcut-scope="footer-code">
164+
<MonacoEditor
165+
value={footerEditorValue}
166+
onChange={onFooterEditorValueChange}
167+
language={footerEditorLanguage}
168+
height="100%"
169+
/>
170+
</div>
171+
</div>
172+
</ResizablePanel>
173+
</ResizablePanelGroup>
174+
) : (
175+
<PreviewSurface
176+
deviceMode={deviceMode}
177+
iframeRef={iframeRef}
178+
overlayContainerRef={overlayContainerRef}
179+
previewUrl={previewUrl}
180+
previewDocument={previewDocument}
181+
pageTitle={pageTitle}
182+
onPreviewLoad={onPreviewLoad}
183+
/>
184+
)}
185+
</>
186+
)}
187+
188+
<div className="flex items-center justify-between border-t border-border/60 bg-background px-2 py-1">
189+
<div className="flex items-center gap-1">
190+
<Button
191+
variant={footerEditorOpen && footerEditorTab === 'html' ? 'secondary' : 'ghost'}
192+
size="sm"
193+
onClick={() => onOpenFooterEditor('html')}
194+
className="h-6 gap-1 px-2 text-[11px]"
195+
>
196+
<CodeXmlIcon className="size-3" />
197+
HTML
198+
</Button>
199+
<Button
200+
variant={footerEditorOpen && footerEditorTab === 'css' ? 'secondary' : 'ghost'}
201+
size="sm"
202+
onClick={() => onOpenFooterEditor('css')}
203+
className="h-6 gap-1 px-2 text-[11px]"
204+
>
205+
<PaintbrushIcon className="size-3" />
206+
CSS
207+
</Button>
208+
<Button
209+
variant={footerEditorOpen && footerEditorTab === 'js' ? 'secondary' : 'ghost'}
210+
size="sm"
211+
onClick={() => onOpenFooterEditor('js')}
212+
className="h-6 gap-1 px-2 text-[11px]"
213+
>
214+
<BracesIcon className="size-3" />
215+
JS
216+
</Button>
217+
</div>
218+
<span className="text-[10px] text-muted-foreground">
219+
{footerEditorOpen ? `${footerEditorTitle} editor open` : 'Open code editor'}
220+
</span>
221+
</div>
222+
</div>
223+
</div>
224+
);
225+
}
226+
227+
function PreviewSurface({
228+
deviceMode,
229+
iframeRef,
230+
overlayContainerRef,
231+
previewUrl,
232+
previewDocument,
233+
pageTitle,
234+
onPreviewLoad,
235+
}: {
236+
deviceMode: BuilderDeviceMode;
237+
iframeRef: RefObject<HTMLIFrameElement | null>;
238+
overlayContainerRef: RefObject<HTMLDivElement | null>;
239+
previewUrl: string | null;
240+
previewDocument: string;
241+
pageTitle: string;
242+
onPreviewLoad: () => void;
243+
}) {
244+
return (
245+
<div className="relative h-full min-h-0 overflow-hidden bg-[#f0f2f5] p-1.5 sm:p-2 lg:bg-transparent lg:p-0">
246+
<BuilderPreviewPanel
247+
deviceMode={deviceMode}
248+
iframeRef={iframeRef}
249+
onLoad={onPreviewLoad}
250+
previewUrl={previewUrl}
251+
previewHtml={previewDocument || undefined}
252+
title={`Builder preview for ${pageTitle}`}
253+
/>
254+
<div ref={overlayContainerRef} className="pointer-events-none absolute inset-0 z-50" />
255+
</div>
256+
);
257+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { MonacoEditor } from '@/components/code-editor/monaco-editor';
2+
import { Button } from '@/components/ui/button';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
} from '@/components/ui/dialog';
10+
11+
type BuilderCodeEditorDialogProps = {
12+
open: boolean;
13+
value: string;
14+
onChange: (value: string) => void;
15+
onClose: () => void;
16+
onApply: () => void;
17+
};
18+
19+
export function BuilderCodeEditorDialog({
20+
open,
21+
value,
22+
onChange,
23+
onClose,
24+
onApply,
25+
}: BuilderCodeEditorDialogProps) {
26+
return (
27+
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose(); }}>
28+
<DialogContent className="flex h-[calc(100vh-4rem)] max-h-[calc(100vh-4rem)] flex-col gap-3 p-3 sm:w-[min(calc(100vw-4rem),72rem)] sm:max-w-6xl">
29+
<DialogHeader className="gap-1">
30+
<DialogTitle>Edit Element Code</DialogTitle>
31+
</DialogHeader>
32+
<div className="min-h-0 flex-1" data-builder-shortcut-scope="element-code">
33+
<MonacoEditor
34+
value={value}
35+
onChange={onChange}
36+
language="html"
37+
height="100%"
38+
/>
39+
</div>
40+
<DialogFooter className="-mx-3 -mb-3 gap-2 rounded-b-xl border-t bg-muted/35 px-3 py-2 sm:px-3">
41+
<Button variant="outline" onClick={onClose}>Cancel</Button>
42+
<Button onClick={onApply}>Apply</Button>
43+
</DialogFooter>
44+
</DialogContent>
45+
</Dialog>
46+
);
47+
}

0 commit comments

Comments
 (0)