@jimmy_harika/websitekit 0.5.4 → 0.5.6
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/EditorDock.tsx +19 -6
- package/src/editor/InsertPoint.tsx +3 -3
- package/src/editor/Inspector.tsx +39 -21
- package/src/editor/PageBuilder.tsx +90 -39
- package/src/editor/SectionFrame.tsx +6 -6
- package/src/editor/SectionsPanel.tsx +25 -7
- package/src/editor/ThemePanel.tsx +1 -1
- package/src/editor/edit-primitives.tsx +6 -6
- package/src/editor/index.tsx +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jimmy_harika/websitekit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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
|
@@ -89,7 +89,7 @@ export function Canvas({ catalog }: { catalog: Catalog }) {
|
|
|
89
89
|
e.stopPropagation();
|
|
90
90
|
ui.openLibrary(0);
|
|
91
91
|
}}
|
|
92
|
-
className="flex min-h-[60vh] w-full flex-col items-center justify-center gap-3 text-muted-foreground/70 transition hover:text-
|
|
92
|
+
className="flex min-h-[60vh] w-full flex-col items-center justify-center gap-3 text-muted-foreground/70 transition hover:text-primary"
|
|
93
93
|
>
|
|
94
94
|
<span className="flex size-12 items-center justify-center rounded-full border-2 border-dashed border-current">
|
|
95
95
|
<Plus className="size-6" />
|
|
@@ -9,29 +9,42 @@ import type { ReactNode } from "react";
|
|
|
9
9
|
import { X } from "lucide-react";
|
|
10
10
|
import { cn } from "../lib/cn";
|
|
11
11
|
|
|
12
|
+
export type EditorDockSize = "default" | "wide" | "xl";
|
|
13
|
+
|
|
14
|
+
const DOCK_WIDTH: Record<EditorDockSize, string> = {
|
|
15
|
+
default: "w-[min(22rem,100vw)]",
|
|
16
|
+
wide: "w-[min(32rem,100vw)]",
|
|
17
|
+
xl: "w-[min(40rem,100vw)]",
|
|
18
|
+
};
|
|
19
|
+
|
|
12
20
|
export function EditorDock({
|
|
13
21
|
title,
|
|
14
22
|
subtitle,
|
|
15
23
|
onClose,
|
|
16
24
|
children,
|
|
17
25
|
wide,
|
|
26
|
+
size,
|
|
18
27
|
className,
|
|
19
28
|
}: {
|
|
20
29
|
title: string;
|
|
21
30
|
subtitle?: string;
|
|
22
31
|
onClose?: () => void;
|
|
23
32
|
children: ReactNode;
|
|
24
|
-
/** Wider dock for Add Block library
|
|
33
|
+
/** @deprecated Prefer `size`. Wider dock for Add Block library. */
|
|
25
34
|
wide?: boolean;
|
|
35
|
+
/** default ~22rem · wide ~32rem · xl ~40rem (SEO / plan managers). */
|
|
36
|
+
size?: EditorDockSize;
|
|
26
37
|
className?: string;
|
|
27
38
|
}) {
|
|
39
|
+
const resolved: EditorDockSize = size ?? (wide ? "wide" : "default");
|
|
28
40
|
return (
|
|
29
41
|
<aside
|
|
30
42
|
data-editor-dock
|
|
43
|
+
data-size={resolved}
|
|
31
44
|
className={cn(
|
|
32
|
-
"flex h-full shrink-0 flex-col border-l border-border/50 bg-card/90 text-foreground shadow-
|
|
33
|
-
|
|
34
|
-
"max-md:fixed max-md:inset-x-0 max-md:bottom-0 max-md:top-auto max-md:z-50 max-md:h-[min(
|
|
45
|
+
"flex h-full min-h-0 shrink-0 flex-col border-l border-border/50 bg-card/90 text-foreground shadow-none backdrop-blur-2xl",
|
|
46
|
+
DOCK_WIDTH[resolved],
|
|
47
|
+
"max-md:fixed max-md:inset-x-0 max-md:bottom-0 max-md:top-auto max-md:z-50 max-md:h-[min(85vh,100%)] max-md:w-full max-md:rounded-t-2xl max-md:border-l-0 max-md:border-t max-md:shadow-2xl",
|
|
35
48
|
className,
|
|
36
49
|
)}
|
|
37
50
|
>
|
|
@@ -79,7 +92,7 @@ export function EditorRail({
|
|
|
79
92
|
data-editor-rail
|
|
80
93
|
aria-label="Editor tools"
|
|
81
94
|
className={cn(
|
|
82
|
-
"z-20 hidden w-[4.25rem] shrink-0 flex-col items-stretch gap-0.5 border-r border-border
|
|
95
|
+
"z-20 hidden w-[4.25rem] shrink-0 flex-col items-stretch gap-0.5 border-r border-border bg-card/80 py-2 backdrop-blur-xl md:flex",
|
|
83
96
|
className,
|
|
84
97
|
)}
|
|
85
98
|
>
|
|
@@ -92,7 +105,7 @@ export function EditorRail({
|
|
|
92
105
|
className={cn(
|
|
93
106
|
"mx-1 flex flex-col items-center gap-0.5 rounded-xl px-1 py-2 text-[9px] font-medium uppercase tracking-[0.08em] transition",
|
|
94
107
|
item.active
|
|
95
|
-
? "bg-
|
|
108
|
+
? "bg-primary text-primary-foreground shadow-sm"
|
|
96
109
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
97
110
|
)}
|
|
98
111
|
>
|
|
@@ -12,19 +12,19 @@ export function InsertPoint({ index }: { index: number }) {
|
|
|
12
12
|
{/* Always visible on touch/small screens (group-hover doesn't fire on touch);
|
|
13
13
|
hover-reveal on desktop. Without this a mobile user can't add a 2nd section. */}
|
|
14
14
|
<div className="pointer-events-none absolute inset-x-0 -top-4 flex h-8 items-center justify-center px-6 opacity-100 transition group-hover/ins:opacity-100 md:opacity-0 md:group-hover/ins:opacity-100">
|
|
15
|
-
<div className="h-px flex-1 bg-
|
|
15
|
+
<div className="h-px flex-1 bg-primary/70" />
|
|
16
16
|
<button
|
|
17
17
|
type="button"
|
|
18
18
|
onClick={(e) => {
|
|
19
19
|
e.stopPropagation();
|
|
20
20
|
ui.openLibrary(index);
|
|
21
21
|
}}
|
|
22
|
-
className="pointer-events-auto mx-2 flex size-7 items-center justify-center rounded-full bg-
|
|
22
|
+
className="pointer-events-auto mx-2 flex size-7 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-md transition hover:scale-105 hover:bg-primary/90"
|
|
23
23
|
title="Add section here"
|
|
24
24
|
>
|
|
25
25
|
<Plus className="size-4" />
|
|
26
26
|
</button>
|
|
27
|
-
<div className="h-px flex-1 bg-
|
|
27
|
+
<div className="h-px flex-1 bg-primary/70" />
|
|
28
28
|
</div>
|
|
29
29
|
</div>
|
|
30
30
|
);
|
package/src/editor/Inspector.tsx
CHANGED
|
@@ -17,7 +17,14 @@ const BP: { id: Breakpoint; icon: typeof Monitor; label: string }[] = [
|
|
|
17
17
|
{ id: "desktop", icon: Monitor, label: "Desktop" },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
export function Inspector({
|
|
20
|
+
export function Inspector({
|
|
21
|
+
catalog,
|
|
22
|
+
/** When true (docked mode), skip the outer chrome — parent dock owns the title bar. */
|
|
23
|
+
embedded,
|
|
24
|
+
}: {
|
|
25
|
+
catalog: Catalog;
|
|
26
|
+
embedded?: boolean;
|
|
27
|
+
}) {
|
|
21
28
|
const ui = useEditorUi();
|
|
22
29
|
const selectedId = useBuilder((s) => s.selectedId);
|
|
23
30
|
const section = useBuilder(
|
|
@@ -29,7 +36,9 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
|
|
|
29
36
|
const remove = useBuilder((s) => s.removeSection);
|
|
30
37
|
const duplicate = useBuilder((s) => s.duplicateSection);
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
// In embedded/dock mode the parent already decided to show us — don't require inspectorOpen.
|
|
40
|
+
if (!embedded && !ui.inspectorOpen) return null;
|
|
41
|
+
if (!section) return null;
|
|
33
42
|
const def = getDefinition(catalog, section.type);
|
|
34
43
|
if (!def) return null;
|
|
35
44
|
|
|
@@ -43,18 +52,27 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
|
|
|
43
52
|
|
|
44
53
|
return (
|
|
45
54
|
<div className="flex h-full min-h-0 flex-col">
|
|
46
|
-
|
|
47
|
-
<div>
|
|
48
|
-
<
|
|
49
|
-
|
|
55
|
+
{!embedded && (
|
|
56
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
57
|
+
<div>
|
|
58
|
+
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
|
59
|
+
Section
|
|
60
|
+
</p>
|
|
61
|
+
<h3 className="text-sm font-semibold text-foreground">{def.label}</h3>
|
|
62
|
+
</div>
|
|
63
|
+
<button
|
|
64
|
+
onClick={() => ui.setInspectorOpen(false)}
|
|
65
|
+
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
66
|
+
>
|
|
67
|
+
<X className="size-4" />
|
|
68
|
+
</button>
|
|
50
69
|
</div>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</div>
|
|
70
|
+
)}
|
|
71
|
+
{embedded && (
|
|
72
|
+
<div className="border-b border-border px-4 py-2.5">
|
|
73
|
+
<p className="text-sm font-semibold text-foreground">{def.label}</p>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
58
76
|
<div className="flex-1 space-y-5 overflow-auto p-4">
|
|
59
77
|
<div>
|
|
60
78
|
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
|
@@ -137,7 +155,7 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
|
|
|
137
155
|
</button>
|
|
138
156
|
<button
|
|
139
157
|
onClick={() => remove(section.id)}
|
|
140
|
-
className="flex-1 rounded-md border border-
|
|
158
|
+
className="flex-1 rounded-md border border-destructive/30 px-3 py-2 text-xs text-destructive hover:bg-destructive/10"
|
|
141
159
|
>
|
|
142
160
|
Delete
|
|
143
161
|
</button>
|
|
@@ -328,7 +346,7 @@ function ImageField({
|
|
|
328
346
|
<img
|
|
329
347
|
src={value}
|
|
330
348
|
alt={altValue ?? ""}
|
|
331
|
-
className={`h-24 w-full rounded-md object-cover ${dragOver ? "ring-2 ring-
|
|
349
|
+
className={`h-24 w-full rounded-md object-cover ${dragOver ? "ring-2 ring-primary" : ""}`}
|
|
332
350
|
onDragOver={(e) => {
|
|
333
351
|
e.preventDefault();
|
|
334
352
|
setDragOver(true);
|
|
@@ -344,7 +362,7 @@ function ImageField({
|
|
|
344
362
|
) : (
|
|
345
363
|
<div
|
|
346
364
|
className={`flex h-24 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground/70 ${
|
|
347
|
-
dragOver ? "border-
|
|
365
|
+
dragOver ? "border-primary bg-primary/10" : ""
|
|
348
366
|
}`}
|
|
349
367
|
onDragOver={(e) => {
|
|
350
368
|
e.preventDefault();
|
|
@@ -377,7 +395,7 @@ function ImageField({
|
|
|
377
395
|
</button>
|
|
378
396
|
)}
|
|
379
397
|
</div>
|
|
380
|
-
{err && <p className="text-[10px] text-
|
|
398
|
+
{err && <p className="text-[10px] text-destructive">{err}</p>}
|
|
381
399
|
<input ref={ref} type="file" accept="image/*,.heic,.heif,image/heic,image/heif" hidden onChange={pick} />
|
|
382
400
|
<input
|
|
383
401
|
value={value}
|
|
@@ -482,7 +500,7 @@ function ImageListField({
|
|
|
482
500
|
<div key={`${url}-${i}`} className="group relative">
|
|
483
501
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
484
502
|
<img src={url} alt="" className="aspect-square w-full rounded-md object-cover" />
|
|
485
|
-
<div className="absolute inset-0 hidden items-end justify-between rounded-md bg-
|
|
503
|
+
<div className="absolute inset-0 hidden items-end justify-between rounded-md bg-foreground/40 p-1 group-hover:flex">
|
|
486
504
|
<div className="flex gap-0.5">
|
|
487
505
|
<button
|
|
488
506
|
title="Move left"
|
|
@@ -515,7 +533,7 @@ function ImageListField({
|
|
|
515
533
|
) : (
|
|
516
534
|
<div
|
|
517
535
|
className={`flex h-16 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground/70 ${
|
|
518
|
-
dragOver ? "border-
|
|
536
|
+
dragOver ? "border-primary bg-primary/10" : ""
|
|
519
537
|
}`}
|
|
520
538
|
onDragOver={(e) => {
|
|
521
539
|
e.preventDefault();
|
|
@@ -535,7 +553,7 @@ function ImageListField({
|
|
|
535
553
|
onClick={() => ref.current?.click()}
|
|
536
554
|
disabled={busy || room === 0}
|
|
537
555
|
className={`w-full rounded-md border px-2 py-1.5 text-xs hover:bg-muted disabled:opacity-50 ${
|
|
538
|
-
dragOver ? "border-
|
|
556
|
+
dragOver ? "border-primary" : ""
|
|
539
557
|
}`}
|
|
540
558
|
onDragOver={(e) => {
|
|
541
559
|
e.preventDefault();
|
|
@@ -670,7 +688,7 @@ function ItemListField({
|
|
|
670
688
|
<button
|
|
671
689
|
title="Remove"
|
|
672
690
|
onClick={() => onChange(value.filter((_, j) => j !== i))}
|
|
673
|
-
className="rounded border px-1 text-[10px] leading-4 text-
|
|
691
|
+
className="rounded border px-1 text-[10px] leading-4 text-destructive"
|
|
674
692
|
>
|
|
675
693
|
×
|
|
676
694
|
</button>
|
|
@@ -50,6 +50,16 @@ export interface HostRailItem {
|
|
|
50
50
|
onClick: () => void;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export type HostDockSize = "default" | "wide" | "xl";
|
|
54
|
+
|
|
55
|
+
export interface HostDock {
|
|
56
|
+
title: string;
|
|
57
|
+
subtitle?: string;
|
|
58
|
+
body: ReactNode;
|
|
59
|
+
/** Wide panels for dense host UIs (SEO, plan). Default fits inspector. */
|
|
60
|
+
size?: HostDockSize;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export interface PageBuilderProps {
|
|
54
64
|
catalog: Catalog;
|
|
55
65
|
initialDocument: PageDocument;
|
|
@@ -59,10 +69,14 @@ export interface PageBuilderProps {
|
|
|
59
69
|
templates?: TemplateMeta[];
|
|
60
70
|
saving?: "idle" | "saving" | "saved";
|
|
61
71
|
leading?: ReactNode;
|
|
72
|
+
/** Site pages switcher — rendered at the top of the Pages dock. */
|
|
73
|
+
pagesSlot?: ReactNode;
|
|
62
74
|
/** Host tools in the labeled left rail (SEO, Domain, …). */
|
|
63
75
|
hostRail?: HostRailItem[];
|
|
64
76
|
/** When set, dock shows this host body (same right dock as all other panels). */
|
|
65
|
-
hostDock?:
|
|
77
|
+
hostDock?: HostDock | null;
|
|
78
|
+
/** Fires when the dock mode changes so hosts can clear host tool selection. */
|
|
79
|
+
onDockChange?: (mode: DockMode) => void;
|
|
66
80
|
viewUrl?: string;
|
|
67
81
|
layout?: "floating" | "classic";
|
|
68
82
|
}
|
|
@@ -86,9 +100,11 @@ function PageBuilderInner({
|
|
|
86
100
|
templates,
|
|
87
101
|
saving,
|
|
88
102
|
leading,
|
|
103
|
+
pagesSlot,
|
|
89
104
|
viewUrl,
|
|
90
105
|
hostRail,
|
|
91
106
|
hostDock,
|
|
107
|
+
onDockChange,
|
|
92
108
|
layout = "floating",
|
|
93
109
|
}: PageBuilderProps) {
|
|
94
110
|
const doc = useBuilder((s) => s.doc);
|
|
@@ -100,24 +116,33 @@ function PageBuilderInner({
|
|
|
100
116
|
const [templatesOpen, setTemplatesOpen] = useState(false);
|
|
101
117
|
const [dock, setDock] = useState<DockMode>("sections");
|
|
102
118
|
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
|
|
119
|
+
const onDockChangeRef = useRef(onDockChange);
|
|
120
|
+
onDockChangeRef.current = onDockChange;
|
|
121
|
+
|
|
122
|
+
const setDockMode = (mode: DockMode) => {
|
|
123
|
+
setDock(mode);
|
|
124
|
+
onDockChangeRef.current?.(mode);
|
|
125
|
+
};
|
|
103
126
|
|
|
104
127
|
// Library open → force library dock. Close library → restore sections.
|
|
105
128
|
useEffect(() => {
|
|
106
|
-
if (libraryIndex !== null)
|
|
129
|
+
if (libraryIndex !== null) setDockMode("library");
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
131
|
}, [libraryIndex]);
|
|
108
132
|
|
|
109
133
|
// Selecting a section opens inspector in the same dock.
|
|
110
134
|
useEffect(() => {
|
|
111
135
|
if (selectedId && libraryIndex === null && !hostDock) {
|
|
112
|
-
|
|
136
|
+
setDockMode("inspector");
|
|
113
137
|
setInspectorOpen(true);
|
|
114
138
|
}
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
140
|
}, [selectedId, libraryIndex, hostDock]);
|
|
116
141
|
|
|
117
142
|
// Host dock takes over the unified dock when provided.
|
|
118
143
|
useEffect(() => {
|
|
119
|
-
if (hostDock)
|
|
120
|
-
else if (dock === "host")
|
|
144
|
+
if (hostDock) setDockMode("host");
|
|
145
|
+
else if (dock === "host") setDockMode("sections");
|
|
121
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
147
|
}, [hostDock]);
|
|
123
148
|
|
|
@@ -128,11 +153,18 @@ function PageBuilderInner({
|
|
|
128
153
|
libraryIndex,
|
|
129
154
|
openLibrary: (index: number | null) => {
|
|
130
155
|
setLibraryIndex(index);
|
|
131
|
-
if (index !== null)
|
|
156
|
+
if (index !== null) {
|
|
157
|
+
setDock("library");
|
|
158
|
+
onDockChangeRef.current?.("library");
|
|
159
|
+
}
|
|
132
160
|
},
|
|
133
161
|
closeLibrary: () => {
|
|
134
162
|
setLibraryIndex(null);
|
|
135
|
-
setDock((d) =>
|
|
163
|
+
setDock((d) => {
|
|
164
|
+
const next = d === "library" ? "sections" : d;
|
|
165
|
+
if (next !== d) onDockChangeRef.current?.(next);
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
136
168
|
},
|
|
137
169
|
inspectorOpen,
|
|
138
170
|
setInspectorOpen,
|
|
@@ -167,20 +199,20 @@ function PageBuilderInner({
|
|
|
167
199
|
}
|
|
168
200
|
// Toggle off if same dock re-clicked
|
|
169
201
|
if (mode !== null && mode === dock) {
|
|
170
|
-
|
|
202
|
+
setDockMode(null);
|
|
171
203
|
return;
|
|
172
204
|
}
|
|
173
205
|
// Leaving library
|
|
174
206
|
if (libraryIndex !== null) {
|
|
175
207
|
setLibraryIndex(null);
|
|
176
208
|
}
|
|
177
|
-
|
|
209
|
+
setDockMode(mode);
|
|
178
210
|
if (mode === "inspector") setInspectorOpen(true);
|
|
179
211
|
};
|
|
180
212
|
|
|
181
213
|
const closeDock = () => {
|
|
182
214
|
if (libraryIndex !== null) ui.closeLibrary();
|
|
183
|
-
|
|
215
|
+
setDockMode(null);
|
|
184
216
|
select(null);
|
|
185
217
|
};
|
|
186
218
|
|
|
@@ -228,7 +260,8 @@ function PageBuilderInner({
|
|
|
228
260
|
id: h.id,
|
|
229
261
|
label: h.label,
|
|
230
262
|
icon: h.icon,
|
|
231
|
-
|
|
263
|
+
// Only the clicked host tool is active — never light up every host item.
|
|
264
|
+
active: !!h.active,
|
|
232
265
|
onClick: () => {
|
|
233
266
|
h.onClick();
|
|
234
267
|
},
|
|
@@ -304,28 +337,37 @@ function PageBuilderInner({
|
|
|
304
337
|
title={dockTitle}
|
|
305
338
|
subtitle={dockSubtitle}
|
|
306
339
|
onClose={closeDock}
|
|
307
|
-
|
|
340
|
+
size={
|
|
341
|
+
dock === "library"
|
|
342
|
+
? "wide"
|
|
343
|
+
: dock === "host"
|
|
344
|
+
? (hostDock?.size ?? "default")
|
|
345
|
+
: "default"
|
|
346
|
+
}
|
|
308
347
|
>
|
|
309
|
-
{dock === "sections" &&
|
|
348
|
+
{dock === "sections" && (
|
|
349
|
+
<SectionsPanel catalog={catalog} embedded pagesSlot={pagesSlot} />
|
|
350
|
+
)}
|
|
310
351
|
{dock === "design" && <ThemePanel />}
|
|
311
|
-
{dock === "inspector" &&
|
|
312
|
-
|
|
313
|
-
{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
352
|
+
{dock === "inspector" &&
|
|
353
|
+
(selectedId ? (
|
|
354
|
+
<Inspector catalog={catalog} embedded />
|
|
355
|
+
) : (
|
|
356
|
+
<p className="p-4 text-sm text-muted-foreground">
|
|
357
|
+
Click a section on the page to edit its content and layout.
|
|
358
|
+
</p>
|
|
359
|
+
))}
|
|
360
|
+
{dock === "library" && <BlockLibraryBody catalog={catalog} />}
|
|
361
|
+
{dock === "host" && (
|
|
362
|
+
<div className="min-h-0 flex-1 overflow-auto p-3 sm:p-4">
|
|
363
|
+
{hostDock?.body}
|
|
320
364
|
</div>
|
|
321
365
|
)}
|
|
322
|
-
{dock === "library" && <BlockLibraryBody catalog={catalog} />}
|
|
323
|
-
{dock === "host" && hostDock?.body}
|
|
324
366
|
</EditorDock>
|
|
325
367
|
)}
|
|
326
368
|
</div>
|
|
327
369
|
|
|
328
|
-
<MobileBar onOpenDock={openDock} />
|
|
370
|
+
<MobileBar dock={dock} onOpenDock={openDock} />
|
|
329
371
|
{templates && templates.length > 0 && (
|
|
330
372
|
<TemplateGallery templates={templates} />
|
|
331
373
|
)}
|
|
@@ -336,8 +378,10 @@ function PageBuilderInner({
|
|
|
336
378
|
}
|
|
337
379
|
|
|
338
380
|
function MobileBar({
|
|
381
|
+
dock,
|
|
339
382
|
onOpenDock,
|
|
340
383
|
}: {
|
|
384
|
+
dock: DockMode;
|
|
341
385
|
onOpenDock: (m: DockMode) => void;
|
|
342
386
|
}) {
|
|
343
387
|
const ui = useEditorUi();
|
|
@@ -351,20 +395,27 @@ function MobileBar({
|
|
|
351
395
|
["design", "Design", <Palette className="size-5" key="d" />],
|
|
352
396
|
["inspector", "Edit", <Sliders className="size-5" key="i" />],
|
|
353
397
|
] as const
|
|
354
|
-
).map(([mode, label, icon]) =>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
398
|
+
).map(([mode, label, icon]) => {
|
|
399
|
+
const active = dock === mode;
|
|
400
|
+
return (
|
|
401
|
+
<button
|
|
402
|
+
key={mode}
|
|
403
|
+
type="button"
|
|
404
|
+
onClick={() => {
|
|
405
|
+
if (mode === "library") ui.openLibrary(sections.length);
|
|
406
|
+
else onOpenDock(mode);
|
|
407
|
+
}}
|
|
408
|
+
className={
|
|
409
|
+
active
|
|
410
|
+
? "flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] font-semibold text-primary"
|
|
411
|
+
: "flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] text-muted-foreground"
|
|
412
|
+
}
|
|
413
|
+
>
|
|
414
|
+
{icon}
|
|
415
|
+
<span>{label}</span>
|
|
416
|
+
</button>
|
|
417
|
+
);
|
|
418
|
+
})}
|
|
368
419
|
</nav>
|
|
369
420
|
);
|
|
370
421
|
}
|
|
@@ -61,14 +61,14 @@ export function SectionFrame({
|
|
|
61
61
|
className={cn(
|
|
62
62
|
"pointer-events-none absolute inset-0 z-10 ring-inset transition",
|
|
63
63
|
selected
|
|
64
|
-
? "ring-2 ring-
|
|
65
|
-
: "ring-1 ring-transparent group-hover/sec:ring-2 group-hover/sec:ring-
|
|
66
|
-
over && "ring-2 ring-
|
|
64
|
+
? "ring-2 ring-primary"
|
|
65
|
+
: "ring-1 ring-transparent group-hover/sec:ring-2 group-hover/sec:ring-primary/40",
|
|
66
|
+
over && "ring-2 ring-primary",
|
|
67
67
|
)}
|
|
68
68
|
/>
|
|
69
69
|
<div
|
|
70
70
|
className={cn(
|
|
71
|
-
"absolute right-3 top-3 z-20 flex items-center gap-0.5 rounded-lg border border-
|
|
71
|
+
"absolute right-3 top-3 z-20 flex items-center gap-0.5 rounded-lg border border-border bg-card/95 p-1 text-foreground shadow-sm backdrop-blur transition",
|
|
72
72
|
selected
|
|
73
73
|
? "opacity-100"
|
|
74
74
|
: "pointer-events-none opacity-0 group-hover/sec:pointer-events-auto group-hover/sec:opacity-100",
|
|
@@ -103,7 +103,7 @@ export function SectionFrame({
|
|
|
103
103
|
<button
|
|
104
104
|
title="Delete"
|
|
105
105
|
onClick={stop(() => remove(section.id))}
|
|
106
|
-
className="rounded p-1.5 text-
|
|
106
|
+
className="rounded p-1.5 text-destructive hover:bg-destructive/10"
|
|
107
107
|
>
|
|
108
108
|
<Trash2 className="size-4" />
|
|
109
109
|
</button>
|
|
@@ -111,7 +111,7 @@ export function SectionFrame({
|
|
|
111
111
|
{section.hideOn && section.hideOn.length > 0 && (
|
|
112
112
|
<div
|
|
113
113
|
title={`Hidden on ${section.hideOn.join(", ")}`}
|
|
114
|
-
className="absolute left-3 top-3 z-20 flex items-center gap-1 rounded-md border border-
|
|
114
|
+
className="absolute left-3 top-3 z-20 flex items-center gap-1 rounded-md border border-border bg-background/95 px-1.5 py-1 text-foreground/70 shadow-sm backdrop-blur"
|
|
115
115
|
>
|
|
116
116
|
<EyeOff className="size-3.5" />
|
|
117
117
|
{section.hideOn.map((bp) => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* reorder; the Add button opens the block library. Reorder rides the same
|
|
7
7
|
* pragmatic-drag-and-drop monitor as the canvas (kind "section").
|
|
8
8
|
*/
|
|
9
|
-
import { useRef, useState } from "react";
|
|
9
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
10
10
|
import { ChevronDown, ChevronUp, GripVertical, Plus, Trash2 } from "lucide-react";
|
|
11
11
|
import { type Catalog, getDefinition } from "../registry";
|
|
12
12
|
import { useBuilder } from "../store";
|
|
@@ -14,19 +14,37 @@ import { useEditorUi } from "./ui-context";
|
|
|
14
14
|
import { useDraggable, useDropTarget } from "./dnd";
|
|
15
15
|
import { cn } from "../lib/cn";
|
|
16
16
|
|
|
17
|
-
export function SectionsPanel({
|
|
17
|
+
export function SectionsPanel({
|
|
18
|
+
catalog,
|
|
19
|
+
embedded,
|
|
20
|
+
pagesSlot,
|
|
21
|
+
}: {
|
|
22
|
+
catalog: Catalog;
|
|
23
|
+
/** Dock owns the title — only show the Add control. */
|
|
24
|
+
embedded?: boolean;
|
|
25
|
+
/** Site page switcher (host). Renders above the block list. */
|
|
26
|
+
pagesSlot?: ReactNode;
|
|
27
|
+
}) {
|
|
18
28
|
const sections = useBuilder((s) => s.doc.sections);
|
|
19
29
|
const ui = useEditorUi();
|
|
20
30
|
|
|
21
31
|
return (
|
|
22
32
|
<div className="flex h-full min-h-0 flex-col">
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
33
|
+
{pagesSlot && (
|
|
34
|
+
<div className="shrink-0 border-b border-border/60 px-3 py-3">{pagesSlot}</div>
|
|
35
|
+
)}
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
"flex items-center border-b border-border px-3 py-2.5",
|
|
39
|
+
embedded ? "justify-between" : "justify-between",
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
43
|
+
Blocks on this page
|
|
26
44
|
</span>
|
|
27
45
|
<button
|
|
28
46
|
onClick={() => ui.openLibrary(sections.length)}
|
|
29
|
-
className="flex items-center gap-1 rounded-md bg-
|
|
47
|
+
className="flex items-center gap-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition hover:opacity-90"
|
|
30
48
|
>
|
|
31
49
|
<Plus className="size-3.5" /> Add
|
|
32
50
|
</button>
|
|
@@ -139,7 +157,7 @@ function SectionRow({
|
|
|
139
157
|
<button
|
|
140
158
|
title="Delete section"
|
|
141
159
|
onClick={stop(() => remove(id))}
|
|
142
|
-
className="rounded p-0.5 text-muted-foreground hover:text-
|
|
160
|
+
className="rounded p-0.5 text-muted-foreground hover:text-destructive"
|
|
143
161
|
>
|
|
144
162
|
<Trash2 className="size-3.5" />
|
|
145
163
|
</button>
|
|
@@ -53,7 +53,7 @@ export function ThemePanel() {
|
|
|
53
53
|
{[p.colors.bg, p.colors.text, p.colors.accent].map((c, i) => (
|
|
54
54
|
<span
|
|
55
55
|
key={i}
|
|
56
|
-
className="size-4 rounded-full border border-
|
|
56
|
+
className="size-4 rounded-full border border-border"
|
|
57
57
|
style={{ background: c }}
|
|
58
58
|
/>
|
|
59
59
|
))}
|
|
@@ -62,7 +62,7 @@ function EditableText({
|
|
|
62
62
|
Tag,
|
|
63
63
|
{
|
|
64
64
|
ref,
|
|
65
|
-
className: cn("outline-none focus:ring-2 focus:ring-
|
|
65
|
+
className: cn("outline-none focus:ring-2 focus:ring-primary/60 rounded-sm", className),
|
|
66
66
|
style,
|
|
67
67
|
contentEditable: true,
|
|
68
68
|
suppressContentEditableWarning: true,
|
|
@@ -175,8 +175,8 @@ function EditableImage({ field, src, alt, className, style, width, height, prior
|
|
|
175
175
|
loading={priority ? "eager" : "lazy"}
|
|
176
176
|
fetchPriority={priority ? "high" : "auto"}
|
|
177
177
|
className={cn(
|
|
178
|
-
"cursor-pointer outline-2 outline-offset-2 outline-transparent hover:outline-
|
|
179
|
-
dragOver && "outline-
|
|
178
|
+
"cursor-pointer outline-2 outline-offset-2 outline-transparent hover:outline-primary",
|
|
179
|
+
dragOver && "outline-primary",
|
|
180
180
|
className,
|
|
181
181
|
)}
|
|
182
182
|
style={style}
|
|
@@ -187,8 +187,8 @@ function EditableImage({ field, src, alt, className, style, width, height, prior
|
|
|
187
187
|
) : (
|
|
188
188
|
<div
|
|
189
189
|
className={cn(
|
|
190
|
-
"flex cursor-pointer items-center justify-center bg-
|
|
191
|
-
dragOver && "outline-
|
|
190
|
+
"flex cursor-pointer items-center justify-center bg-foreground/[0.04] text-[11px] uppercase tracking-wider opacity-50 outline-2 outline-dashed outline-foreground/15 hover:outline-primary",
|
|
191
|
+
dragOver && "outline-primary opacity-80",
|
|
192
192
|
className,
|
|
193
193
|
)}
|
|
194
194
|
style={style}
|
|
@@ -199,7 +199,7 @@ function EditableImage({ field, src, alt, className, style, width, height, prior
|
|
|
199
199
|
</div>
|
|
200
200
|
)}
|
|
201
201
|
{err && (
|
|
202
|
-
<p className="mt-1 text-[10px] text-
|
|
202
|
+
<p className="mt-1 text-[10px] text-destructive" title={err}>
|
|
203
203
|
{err}
|
|
204
204
|
</p>
|
|
205
205
|
)}
|
package/src/editor/index.tsx
CHANGED
|
@@ -6,9 +6,11 @@ export {
|
|
|
6
6
|
PageBuilder,
|
|
7
7
|
type PageBuilderProps,
|
|
8
8
|
type HostRailItem,
|
|
9
|
+
type HostDock,
|
|
10
|
+
type HostDockSize,
|
|
9
11
|
type DockMode,
|
|
10
12
|
} from "./PageBuilder";
|
|
11
|
-
export { EditorDock, EditorRail } from "./EditorDock";
|
|
13
|
+
export { EditorDock, EditorRail, type EditorDockSize } from "./EditorDock";
|
|
12
14
|
export { type TemplateMeta } from "./TemplateGallery";
|
|
13
15
|
export { type Device } from "./ui-context";
|
|
14
16
|
export { type EditorCapabilities } from "./edit-primitives";
|