@jimmy_harika/websitekit 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/editor/Canvas.tsx +1 -1
- package/src/editor/PageBuilder.tsx +234 -46
- package/src/editor/Toolbar.tsx +49 -18
- package/src/editor/edit-primitives.tsx +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jimmy_harika/websitekit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Snow Labs website builder engine. Block-agnostic, curated-section. One shared builder across every Snow Labs app — engine + design-system-agnostic blocks + per-industry catalogs (author, photographer, …). Each app plugs in a catalog + theme + persistence adapter.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
package/src/editor/Canvas.tsx
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Top-level editor.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* top toolbar · [design panel] · canvas · [inspector] · overlays (library,
|
|
8
|
-
* templates).
|
|
4
|
+
* Top-level editor. Floating-panel layout: canvas owns the space; left/right
|
|
5
|
+
* tools open as on-call glass cards so the live page stays large (Pixieset-style
|
|
6
|
+
* preview-first). Host apps theme via [data-websitekit-editor] + CSS vars.
|
|
9
7
|
*/
|
|
10
8
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
11
|
-
import { List, Palette, Plus, Sliders, X } from "lucide-react";
|
|
9
|
+
import { List, Palette, PanelRight, Plus, Sliders, X } from "lucide-react";
|
|
12
10
|
import type { PageDocument } from "../model";
|
|
13
11
|
import type { Catalog } from "../registry";
|
|
14
12
|
import { BuilderProvider, useBuilder, useCreateBuilderStore } from "../store";
|
|
@@ -22,21 +20,27 @@ import { ThemePanel } from "./ThemePanel";
|
|
|
22
20
|
import { SectionsPanel } from "./SectionsPanel";
|
|
23
21
|
import { TemplateGallery, type TemplateMeta } from "./TemplateGallery";
|
|
24
22
|
import { useEditorShortcuts } from "./shortcuts";
|
|
23
|
+
import { cn } from "../lib/cn";
|
|
25
24
|
|
|
26
25
|
export interface PageBuilderProps {
|
|
27
26
|
catalog: Catalog;
|
|
28
27
|
initialDocument: PageDocument;
|
|
29
|
-
/** Fires (debounced upstream) whenever the document changes — for autosave. */
|
|
30
28
|
onChange?: (doc: PageDocument) => void;
|
|
31
29
|
onPublish?: (doc: PageDocument) => void | Promise<void>;
|
|
32
|
-
/** Upload a picked file, return its public URL (R2 / Convex storage). */
|
|
33
30
|
uploadImage?: (file: File) => Promise<string>;
|
|
34
31
|
templates?: TemplateMeta[];
|
|
35
32
|
saving?: "idle" | "saving" | "saved";
|
|
36
|
-
/** Header gate content (e.g. exit link + site name), pinned far left. */
|
|
37
33
|
leading?: ReactNode;
|
|
38
|
-
/**
|
|
34
|
+
/** Extra tools in the left icon rail (host app: SEO, Domain, …). */
|
|
35
|
+
railExtra?: ReactNode;
|
|
36
|
+
/** Floating host panel body (when host manages its own panel open state). */
|
|
37
|
+
hostPanel?: ReactNode;
|
|
39
38
|
viewUrl?: string;
|
|
39
|
+
/**
|
|
40
|
+
* floating (default) = max canvas, panels on-call.
|
|
41
|
+
* classic = permanent 3-column rails.
|
|
42
|
+
*/
|
|
43
|
+
layout?: "floating" | "classic";
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export function PageBuilder(props: PageBuilderProps) {
|
|
@@ -51,6 +55,8 @@ export function PageBuilder(props: PageBuilderProps) {
|
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
type LeftPanel = "sections" | "design" | null;
|
|
59
|
+
|
|
54
60
|
function PageBuilderInner({
|
|
55
61
|
catalog,
|
|
56
62
|
onChange,
|
|
@@ -59,15 +65,24 @@ function PageBuilderInner({
|
|
|
59
65
|
saving,
|
|
60
66
|
leading,
|
|
61
67
|
viewUrl,
|
|
68
|
+
railExtra,
|
|
69
|
+
hostPanel,
|
|
70
|
+
layout = "floating",
|
|
62
71
|
}: PageBuilderProps) {
|
|
63
72
|
const doc = useBuilder((s) => s.doc);
|
|
73
|
+
const selectedId = useBuilder((s) => s.selectedId);
|
|
64
74
|
const [device, setDevice] = useState<Device>("desktop");
|
|
65
75
|
const [libraryIndex, setLibraryIndex] = useState<number | null>(null);
|
|
66
|
-
const [inspectorOpen, setInspectorOpen] = useState(
|
|
76
|
+
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
67
77
|
const [templatesOpen, setTemplatesOpen] = useState(false);
|
|
68
|
-
const [
|
|
78
|
+
const [leftPanel, setLeftPanel] = useState<LeftPanel>("sections");
|
|
69
79
|
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
|
|
70
80
|
|
|
81
|
+
// Open inspector when a section is selected (floating mode); keep open in classic.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (layout === "floating" && selectedId) setInspectorOpen(true);
|
|
84
|
+
}, [selectedId, layout]);
|
|
85
|
+
|
|
71
86
|
const ui: EditorUi = useMemo(
|
|
72
87
|
() => ({
|
|
73
88
|
device,
|
|
@@ -85,7 +100,6 @@ function PageBuilderInner({
|
|
|
85
100
|
[device, libraryIndex, inspectorOpen, templatesOpen, mobilePanel],
|
|
86
101
|
);
|
|
87
102
|
|
|
88
|
-
// Autosave: emit document changes (skip the initial render).
|
|
89
103
|
const onChangeRef = useRef(onChange);
|
|
90
104
|
onChangeRef.current = onChange;
|
|
91
105
|
const firstRender = useRef(true);
|
|
@@ -97,55 +111,226 @@ function PageBuilderInner({
|
|
|
97
111
|
onChangeRef.current?.(doc);
|
|
98
112
|
}, [doc]);
|
|
99
113
|
|
|
100
|
-
// Keyboard shortcuts (⌘Z/⌘⇧Z, Delete, ⌘D, Esc, ⌘/, ⌘⇧P, ↑/↓).
|
|
101
|
-
// `ui` is passed in (not read from context) because this hook runs in
|
|
102
|
-
// PageBuilderInner, which sits ABOVE its own <EditorUiContext.Provider>.
|
|
103
114
|
useEditorShortcuts({ ui, onPublish: onPublish ? () => onPublish(doc) : undefined });
|
|
104
115
|
|
|
116
|
+
const toggleLeft = (p: LeftPanel) =>
|
|
117
|
+
setLeftPanel((cur) => (cur === p ? null : p));
|
|
118
|
+
|
|
119
|
+
if (layout === "classic") {
|
|
120
|
+
return (
|
|
121
|
+
<EditorUiContext.Provider value={ui}>
|
|
122
|
+
<div data-websitekit-editor data-layout="classic" className="flex h-full min-h-0 flex-col bg-muted">
|
|
123
|
+
<Toolbar
|
|
124
|
+
onPublish={onPublish ? () => onPublish(doc) : undefined}
|
|
125
|
+
saving={saving}
|
|
126
|
+
designOpen={leftPanel === "design"}
|
|
127
|
+
onToggleDesign={() => toggleLeft("design")}
|
|
128
|
+
hasTemplates={!!templates && templates.length > 0}
|
|
129
|
+
leading={leading}
|
|
130
|
+
viewUrl={viewUrl}
|
|
131
|
+
/>
|
|
132
|
+
<div className="flex min-h-0 flex-1">
|
|
133
|
+
<aside className="hidden w-64 shrink-0 flex-col border-r border-border bg-card md:flex">
|
|
134
|
+
{leftPanel === "design" ? (
|
|
135
|
+
<>
|
|
136
|
+
<div className="border-b border-border px-3 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
137
|
+
Design
|
|
138
|
+
</div>
|
|
139
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
140
|
+
<ThemePanel />
|
|
141
|
+
</div>
|
|
142
|
+
</>
|
|
143
|
+
) : (
|
|
144
|
+
<SectionsPanel catalog={catalog} />
|
|
145
|
+
)}
|
|
146
|
+
</aside>
|
|
147
|
+
<Canvas catalog={catalog} />
|
|
148
|
+
<aside className="hidden w-80 shrink-0 flex-col border-l border-border bg-card md:flex">
|
|
149
|
+
<Inspector catalog={catalog} />
|
|
150
|
+
</aside>
|
|
151
|
+
</div>
|
|
152
|
+
<MobileBar />
|
|
153
|
+
<MobileSheet catalog={catalog} />
|
|
154
|
+
<BlockLibrary catalog={catalog} />
|
|
155
|
+
{templates && templates.length > 0 && <TemplateGallery templates={templates} />}
|
|
156
|
+
<EditorStyle />
|
|
157
|
+
</div>
|
|
158
|
+
</EditorUiContext.Provider>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Floating layout (default): max canvas, panels on-call ──────────────
|
|
105
163
|
return (
|
|
106
164
|
<EditorUiContext.Provider value={ui}>
|
|
107
|
-
<div
|
|
165
|
+
<div
|
|
166
|
+
data-websitekit-editor
|
|
167
|
+
data-layout="floating"
|
|
168
|
+
className="relative flex h-full min-h-0 flex-col bg-transparent"
|
|
169
|
+
>
|
|
108
170
|
<Toolbar
|
|
109
171
|
onPublish={onPublish ? () => onPublish(doc) : undefined}
|
|
110
172
|
saving={saving}
|
|
111
|
-
designOpen={
|
|
112
|
-
onToggleDesign={() =>
|
|
173
|
+
designOpen={leftPanel === "design"}
|
|
174
|
+
onToggleDesign={() => toggleLeft("design")}
|
|
113
175
|
hasTemplates={!!templates && templates.length > 0}
|
|
114
176
|
leading={leading}
|
|
115
177
|
viewUrl={viewUrl}
|
|
178
|
+
floating
|
|
179
|
+
inspectorOpen={inspectorOpen}
|
|
180
|
+
onToggleInspector={() => setInspectorOpen((v) => !v)}
|
|
181
|
+
sectionsOpen={leftPanel === "sections"}
|
|
182
|
+
onToggleSections={() => toggleLeft("sections")}
|
|
116
183
|
/>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
184
|
+
|
|
185
|
+
<div className="relative flex min-h-0 flex-1">
|
|
186
|
+
{/* Icon rail — always present, thin */}
|
|
187
|
+
<nav
|
|
188
|
+
className="z-30 hidden w-12 shrink-0 flex-col items-center gap-1 border-r border-border/40 bg-card/40 py-2 backdrop-blur-xl md:flex"
|
|
189
|
+
aria-label="Editor tools"
|
|
190
|
+
>
|
|
191
|
+
<RailBtn
|
|
192
|
+
active={leftPanel === "sections"}
|
|
193
|
+
onClick={() => toggleLeft("sections")}
|
|
194
|
+
title="Sections"
|
|
195
|
+
>
|
|
196
|
+
<List className="size-4" />
|
|
197
|
+
</RailBtn>
|
|
198
|
+
<RailBtn
|
|
199
|
+
active={leftPanel === "design"}
|
|
200
|
+
onClick={() => toggleLeft("design")}
|
|
201
|
+
title="Design"
|
|
202
|
+
>
|
|
203
|
+
<Palette className="size-4" />
|
|
204
|
+
</RailBtn>
|
|
205
|
+
<RailBtn
|
|
206
|
+
active={false}
|
|
207
|
+
onClick={() => ui.openLibrary(doc.sections.length)}
|
|
208
|
+
title="Add block"
|
|
209
|
+
>
|
|
210
|
+
<Plus className="size-4" />
|
|
211
|
+
</RailBtn>
|
|
212
|
+
<RailBtn
|
|
213
|
+
active={inspectorOpen}
|
|
214
|
+
onClick={() => setInspectorOpen((v) => !v)}
|
|
215
|
+
title="Inspector"
|
|
216
|
+
>
|
|
217
|
+
<PanelRight className="size-4" />
|
|
218
|
+
</RailBtn>
|
|
219
|
+
{railExtra}
|
|
220
|
+
</nav>
|
|
221
|
+
|
|
222
|
+
{/* Canvas fills everything else */}
|
|
223
|
+
<div className="relative min-w-0 flex-1">
|
|
224
|
+
<Canvas catalog={catalog} />
|
|
225
|
+
|
|
226
|
+
{/* Floating left panel */}
|
|
227
|
+
{leftPanel && (
|
|
228
|
+
<FloatingCard side="left" onClose={() => setLeftPanel(null)} title={leftPanel === "design" ? "Design" : "Sections"}>
|
|
229
|
+
{leftPanel === "design" ? <ThemePanel /> : <SectionsPanel catalog={catalog} />}
|
|
230
|
+
</FloatingCard>
|
|
130
231
|
)}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
232
|
+
|
|
233
|
+
{/* Host panel (SEO / Domain / …) */}
|
|
234
|
+
{hostPanel}
|
|
235
|
+
|
|
236
|
+
{/* Floating inspector — only when open */}
|
|
237
|
+
{inspectorOpen && (
|
|
238
|
+
<FloatingCard
|
|
239
|
+
side="right"
|
|
240
|
+
onClose={() => setInspectorOpen(false)}
|
|
241
|
+
title="Inspector"
|
|
242
|
+
wide
|
|
243
|
+
>
|
|
244
|
+
<Inspector catalog={catalog} />
|
|
245
|
+
</FloatingCard>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
137
248
|
</div>
|
|
249
|
+
|
|
138
250
|
<MobileBar />
|
|
139
|
-
<MobileSheet catalog={catalog}
|
|
251
|
+
<MobileSheet catalog={catalog} />
|
|
140
252
|
<BlockLibrary catalog={catalog} />
|
|
141
253
|
{templates && templates.length > 0 && <TemplateGallery templates={templates} />}
|
|
142
|
-
<
|
|
254
|
+
<EditorStyle />
|
|
143
255
|
</div>
|
|
144
256
|
</EditorUiContext.Provider>
|
|
145
257
|
);
|
|
146
258
|
}
|
|
147
259
|
|
|
148
|
-
|
|
260
|
+
function RailBtn({
|
|
261
|
+
active,
|
|
262
|
+
onClick,
|
|
263
|
+
title,
|
|
264
|
+
children,
|
|
265
|
+
}: {
|
|
266
|
+
active: boolean;
|
|
267
|
+
onClick: () => void;
|
|
268
|
+
title: string;
|
|
269
|
+
children: ReactNode;
|
|
270
|
+
}) {
|
|
271
|
+
return (
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
title={title}
|
|
275
|
+
onClick={onClick}
|
|
276
|
+
className={cn(
|
|
277
|
+
"flex size-9 items-center justify-center rounded-xl transition",
|
|
278
|
+
active
|
|
279
|
+
? "bg-foreground text-background shadow-sm"
|
|
280
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
281
|
+
)}
|
|
282
|
+
>
|
|
283
|
+
{children}
|
|
284
|
+
</button>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function FloatingCard({
|
|
289
|
+
side,
|
|
290
|
+
title,
|
|
291
|
+
onClose,
|
|
292
|
+
children,
|
|
293
|
+
wide,
|
|
294
|
+
}: {
|
|
295
|
+
side: "left" | "right";
|
|
296
|
+
title: string;
|
|
297
|
+
onClose: () => void;
|
|
298
|
+
children: ReactNode;
|
|
299
|
+
wide?: boolean;
|
|
300
|
+
}) {
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
className={cn(
|
|
304
|
+
"absolute top-3 bottom-3 z-20 flex flex-col overflow-hidden rounded-2xl border border-border/50 bg-card/85 shadow-2xl backdrop-blur-2xl",
|
|
305
|
+
wide ? "w-[min(22rem,calc(100%-1.5rem))]" : "w-[min(18rem,calc(100%-1.5rem))]",
|
|
306
|
+
side === "left" ? "left-3" : "right-3",
|
|
307
|
+
)}
|
|
308
|
+
data-floating-panel={side}
|
|
309
|
+
>
|
|
310
|
+
<div className="flex items-center justify-between border-b border-border/40 px-3 py-2">
|
|
311
|
+
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
312
|
+
{title}
|
|
313
|
+
</span>
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
onClick={onClose}
|
|
317
|
+
className="rounded-lg p-1 text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
318
|
+
aria-label="Close panel"
|
|
319
|
+
>
|
|
320
|
+
<X className="size-3.5" />
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="min-h-0 flex-1 overflow-auto">{children}</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function EditorStyle() {
|
|
329
|
+
return (
|
|
330
|
+
<style>{`[data-websitekit-editor] [data-pb-field]:empty::before{content:attr(data-pb-placeholder);opacity:.4;pointer-events:none;}`}</style>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
149
334
|
function MobileBar() {
|
|
150
335
|
const ui = useEditorUi();
|
|
151
336
|
const sections = useBuilder((s) => s.doc.sections);
|
|
@@ -163,7 +348,7 @@ function MobileBar() {
|
|
|
163
348
|
</button>
|
|
164
349
|
);
|
|
165
350
|
return (
|
|
166
|
-
<nav className="flex shrink-0 items-stretch border-t border-border bg-card px-1 pb-[env(safe-area-inset-bottom)] md:hidden">
|
|
351
|
+
<nav className="flex shrink-0 items-stretch border-t border-border/50 bg-card/80 px-1 pb-[env(safe-area-inset-bottom)] backdrop-blur-xl md:hidden">
|
|
167
352
|
{tab("sections", "Sections", <List className="size-5" />)}
|
|
168
353
|
<button
|
|
169
354
|
type="button"
|
|
@@ -179,8 +364,7 @@ function MobileBar() {
|
|
|
179
364
|
);
|
|
180
365
|
}
|
|
181
366
|
|
|
182
|
-
|
|
183
|
-
function MobileSheet({ catalog, designOpen }: { catalog: Catalog; designOpen: boolean }) {
|
|
367
|
+
function MobileSheet({ catalog }: { catalog: Catalog }) {
|
|
184
368
|
const ui = useEditorUi();
|
|
185
369
|
if (ui.mobilePanel === null) return null;
|
|
186
370
|
return (
|
|
@@ -190,10 +374,14 @@ function MobileSheet({ catalog, designOpen }: { catalog: Catalog; designOpen: bo
|
|
|
190
374
|
onClick={() => ui.setMobilePanel(null)}
|
|
191
375
|
className="absolute inset-0 bg-black/40"
|
|
192
376
|
/>
|
|
193
|
-
<div className="relative max-h-[70vh] overflow-auto rounded-t-2xl border-t border-border bg-card pb-[env(safe-area-inset-bottom)]">
|
|
377
|
+
<div className="relative max-h-[70vh] overflow-auto rounded-t-2xl border-t border-border bg-card/95 pb-[env(safe-area-inset-bottom)] backdrop-blur-2xl">
|
|
194
378
|
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
195
379
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
196
|
-
{ui.mobilePanel === "sections"
|
|
380
|
+
{ui.mobilePanel === "sections"
|
|
381
|
+
? "Sections"
|
|
382
|
+
: ui.mobilePanel === "design"
|
|
383
|
+
? "Design"
|
|
384
|
+
: "Section"}
|
|
197
385
|
</span>
|
|
198
386
|
<button
|
|
199
387
|
onClick={() => ui.setMobilePanel(null)}
|
package/src/editor/Toolbar.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* device preview · undo/redo · view · save status · publish. Theme-optimized
|
|
5
|
-
* (bg-card surface, semantic tokens). */
|
|
3
|
+
/** Editor header — glass-friendly surface, device switcher, undo/redo, publish. */
|
|
6
4
|
import type { ReactNode } from "react";
|
|
7
5
|
import {
|
|
8
6
|
ExternalLink,
|
|
9
7
|
LayoutGrid,
|
|
8
|
+
List,
|
|
10
9
|
Monitor,
|
|
11
10
|
Palette,
|
|
11
|
+
PanelRight,
|
|
12
12
|
Redo2,
|
|
13
13
|
Smartphone,
|
|
14
14
|
Tablet,
|
|
@@ -32,16 +32,24 @@ export function Toolbar({
|
|
|
32
32
|
hasTemplates,
|
|
33
33
|
leading,
|
|
34
34
|
viewUrl,
|
|
35
|
+
floating,
|
|
36
|
+
inspectorOpen,
|
|
37
|
+
onToggleInspector,
|
|
38
|
+
sectionsOpen,
|
|
39
|
+
onToggleSections,
|
|
35
40
|
}: {
|
|
36
41
|
onPublish?: () => void;
|
|
37
42
|
saving?: "idle" | "saving" | "saved";
|
|
38
43
|
designOpen: boolean;
|
|
39
44
|
onToggleDesign: () => void;
|
|
40
45
|
hasTemplates?: boolean;
|
|
41
|
-
/** Gate content (exit + site name), pinned far left. */
|
|
42
46
|
leading?: ReactNode;
|
|
43
|
-
/** "View live site" link target. */
|
|
44
47
|
viewUrl?: string;
|
|
48
|
+
floating?: boolean;
|
|
49
|
+
inspectorOpen?: boolean;
|
|
50
|
+
onToggleInspector?: () => void;
|
|
51
|
+
sectionsOpen?: boolean;
|
|
52
|
+
onToggleSections?: () => void;
|
|
45
53
|
}) {
|
|
46
54
|
const ui = useEditorUi();
|
|
47
55
|
const undo = useTemporal((s) => s.undo);
|
|
@@ -50,26 +58,49 @@ export function Toolbar({
|
|
|
50
58
|
const canRedo = useTemporal((s) => s.futureStates.length > 0);
|
|
51
59
|
|
|
52
60
|
return (
|
|
53
|
-
<header
|
|
61
|
+
<header
|
|
62
|
+
className={cn(
|
|
63
|
+
"relative z-40 flex h-12 shrink-0 items-center gap-1 px-2 text-foreground sm:px-3",
|
|
64
|
+
floating
|
|
65
|
+
? "border-b border-border/40 bg-card/50 backdrop-blur-xl"
|
|
66
|
+
: "border-b border-border bg-card",
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
54
69
|
{leading && (
|
|
55
|
-
<div className="flex min-w-0 items-center gap-1.5 pr-1.5 sm:border-r sm:border-border">
|
|
70
|
+
<div className="flex min-w-0 items-center gap-1.5 pr-1.5 sm:border-r sm:border-border/40">
|
|
56
71
|
{leading}
|
|
57
72
|
</div>
|
|
58
73
|
)}
|
|
59
74
|
<div className="flex items-center gap-0.5 pl-0.5">
|
|
75
|
+
{floating && onToggleSections && (
|
|
76
|
+
<ToolButton active={!!sectionsOpen} onClick={onToggleSections} title="Sections">
|
|
77
|
+
<List className="size-4" />
|
|
78
|
+
<span className="hidden text-xs lg:inline">Sections</span>
|
|
79
|
+
</ToolButton>
|
|
80
|
+
)}
|
|
60
81
|
<ToolButton active={designOpen} onClick={onToggleDesign} title="Design">
|
|
61
82
|
<Palette className="size-4" />
|
|
62
|
-
<span className="hidden text-xs
|
|
83
|
+
<span className="hidden text-xs lg:inline">Design</span>
|
|
63
84
|
</ToolButton>
|
|
64
85
|
{hasTemplates && (
|
|
65
86
|
<ToolButton onClick={() => ui.setTemplatesOpen(true)} title="Templates">
|
|
66
87
|
<LayoutGrid className="size-4" />
|
|
67
|
-
<span className="hidden text-xs
|
|
88
|
+
<span className="hidden text-xs lg:inline">Templates</span>
|
|
89
|
+
</ToolButton>
|
|
90
|
+
)}
|
|
91
|
+
{floating && onToggleInspector && (
|
|
92
|
+
<ToolButton
|
|
93
|
+
active={!!inspectorOpen}
|
|
94
|
+
onClick={onToggleInspector}
|
|
95
|
+
title="Inspector"
|
|
96
|
+
>
|
|
97
|
+
<PanelRight className="size-4" />
|
|
98
|
+
<span className="hidden text-xs lg:inline">Inspect</span>
|
|
68
99
|
</ToolButton>
|
|
69
100
|
)}
|
|
70
101
|
</div>
|
|
71
102
|
|
|
72
|
-
<div className="absolute left-1/2 flex -translate-x-1/2 items-center rounded-
|
|
103
|
+
<div className="absolute left-1/2 flex -translate-x-1/2 items-center rounded-xl border border-border/50 bg-background/70 p-0.5 backdrop-blur-md">
|
|
73
104
|
{DEVICES.map((d) => {
|
|
74
105
|
const Icon = d.icon;
|
|
75
106
|
return (
|
|
@@ -78,9 +109,9 @@ export function Toolbar({
|
|
|
78
109
|
title={d.label}
|
|
79
110
|
onClick={() => ui.setDevice(d.id)}
|
|
80
111
|
className={cn(
|
|
81
|
-
"rounded-
|
|
112
|
+
"rounded-lg p-1.5 transition",
|
|
82
113
|
ui.device === d.id
|
|
83
|
-
? "bg-secondary text-secondary-foreground"
|
|
114
|
+
? "bg-secondary text-secondary-foreground shadow-sm"
|
|
84
115
|
: "text-muted-foreground hover:bg-muted",
|
|
85
116
|
)}
|
|
86
117
|
>
|
|
@@ -95,7 +126,7 @@ export function Toolbar({
|
|
|
95
126
|
onClick={() => undo()}
|
|
96
127
|
disabled={!canUndo}
|
|
97
128
|
title="Undo"
|
|
98
|
-
className="rounded-
|
|
129
|
+
className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
|
|
99
130
|
>
|
|
100
131
|
<Undo2 className="size-4" />
|
|
101
132
|
</button>
|
|
@@ -103,7 +134,7 @@ export function Toolbar({
|
|
|
103
134
|
onClick={() => redo()}
|
|
104
135
|
disabled={!canRedo}
|
|
105
136
|
title="Redo"
|
|
106
|
-
className="rounded-
|
|
137
|
+
className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
|
|
107
138
|
>
|
|
108
139
|
<Redo2 className="size-4" />
|
|
109
140
|
</button>
|
|
@@ -113,9 +144,9 @@ export function Toolbar({
|
|
|
113
144
|
target="_blank"
|
|
114
145
|
rel="noreferrer"
|
|
115
146
|
title="View live site"
|
|
116
|
-
className="ml-1 hidden items-center gap-1 rounded-
|
|
147
|
+
className="ml-1 hidden items-center gap-1 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:flex"
|
|
117
148
|
>
|
|
118
|
-
<ExternalLink className="size-3.5" />
|
|
149
|
+
<ExternalLink className="size-3.5" /> Preview
|
|
119
150
|
</a>
|
|
120
151
|
)}
|
|
121
152
|
<span className="mx-1 w-12 text-right text-[11px] text-muted-foreground/70">
|
|
@@ -124,7 +155,7 @@ export function Toolbar({
|
|
|
124
155
|
{onPublish && (
|
|
125
156
|
<button
|
|
126
157
|
onClick={onPublish}
|
|
127
|
-
className="rounded-
|
|
158
|
+
className="rounded-xl bg-primary px-3.5 py-1.5 text-sm font-semibold text-primary-foreground shadow-sm transition hover:opacity-90 sm:px-4"
|
|
128
159
|
>
|
|
129
160
|
Publish
|
|
130
161
|
</button>
|
|
@@ -150,7 +181,7 @@ function ToolButton({
|
|
|
150
181
|
onClick={onClick}
|
|
151
182
|
title={title}
|
|
152
183
|
className={cn(
|
|
153
|
-
"flex items-center gap-1.5 rounded-
|
|
184
|
+
"flex items-center gap-1.5 rounded-xl px-2 py-1.5 sm:px-2.5",
|
|
154
185
|
active
|
|
155
186
|
? "bg-secondary text-secondary-foreground"
|
|
156
187
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useRef,
|
|
15
15
|
useState,
|
|
16
16
|
type ChangeEvent,
|
|
17
|
+
type DragEvent as ReactDragEvent,
|
|
17
18
|
type ElementType,
|
|
18
19
|
type FocusEvent as ReactFocusEvent,
|
|
19
20
|
type KeyboardEvent as ReactKeyboardEvent,
|
|
@@ -138,17 +139,17 @@ function EditableImage({ field, src, alt, className, style, width, height, prior
|
|
|
138
139
|
inputRef.current?.click();
|
|
139
140
|
};
|
|
140
141
|
|
|
141
|
-
const onDragOver = (e:
|
|
142
|
+
const onDragOver = (e: ReactDragEvent) => {
|
|
142
143
|
e.preventDefault();
|
|
143
144
|
e.stopPropagation();
|
|
144
145
|
setDragOver(true);
|
|
145
146
|
};
|
|
146
|
-
const onDragLeave = (e:
|
|
147
|
+
const onDragLeave = (e: ReactDragEvent) => {
|
|
147
148
|
e.preventDefault();
|
|
148
149
|
e.stopPropagation();
|
|
149
150
|
setDragOver(false);
|
|
150
151
|
};
|
|
151
|
-
const onDrop = (e:
|
|
152
|
+
const onDrop = (e: ReactDragEvent) => {
|
|
152
153
|
e.preventDefault();
|
|
153
154
|
e.stopPropagation();
|
|
154
155
|
setDragOver(false);
|