@jimmy_harika/websitekit 0.5.3 → 0.5.4
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/BlockLibrary.tsx +82 -54
- package/src/editor/EditorDock.tsx +107 -0
- package/src/editor/PageBuilder.tsx +241 -276
- package/src/editor/Toolbar.tsx +33 -82
- package/src/editor/index.tsx +7 -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.4",
|
|
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,
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* previews
|
|
6
|
-
*
|
|
7
|
-
* you see is what you get.
|
|
4
|
+
* Add Block library — Pixieset-style: category chips + visual grid of live
|
|
5
|
+
* mini-previews. Renders INSIDE the unified right EditorDock (not a random
|
|
6
|
+
* portal side). Click inserts at the open library index.
|
|
8
7
|
*/
|
|
9
|
-
import { createElement,
|
|
10
|
-
import { createPortal } from "react-dom";
|
|
11
|
-
import { X } from "lucide-react";
|
|
8
|
+
import { createElement, useMemo, useState } from "react";
|
|
12
9
|
import {
|
|
13
10
|
type Catalog,
|
|
14
11
|
type SectionDefinition,
|
|
@@ -20,16 +17,20 @@ import { useBuilder } from "../store";
|
|
|
20
17
|
import { useEditorUi } from "./ui-context";
|
|
21
18
|
import { cn } from "../lib/cn";
|
|
22
19
|
|
|
23
|
-
export function
|
|
20
|
+
export function BlockLibraryBody({ catalog }: { catalog: Catalog }) {
|
|
24
21
|
const ui = useEditorUi();
|
|
25
22
|
const theme = useBuilder((s) => s.doc.theme);
|
|
26
23
|
const insertSection = useBuilder((s) => s.insertSection);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const categories = catalog.categories.length
|
|
25
|
+
? catalog.categories
|
|
26
|
+
: ["All"];
|
|
27
|
+
const [active, setActive] = useState(categories[0] ?? "All");
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const items = useMemo(() => {
|
|
30
|
+
const all = Object.values(catalog.sections);
|
|
31
|
+
if (active === "All" || !catalog.categories.length) return all;
|
|
32
|
+
return all.filter((d) => d.category === active);
|
|
33
|
+
}, [catalog, active]);
|
|
33
34
|
|
|
34
35
|
function add(def: SectionDefinition, variant?: string) {
|
|
35
36
|
insertSection(
|
|
@@ -44,67 +45,94 @@ export function BlockLibrary({ catalog }: { catalog: Catalog }) {
|
|
|
44
45
|
ui.closeLibrary();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
48
|
+
if (ui.libraryIndex === null) return null;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
52
|
+
<div className="shrink-0 space-y-2 border-b border-border/40 px-3 py-3">
|
|
53
|
+
<p className="text-xs text-muted-foreground">
|
|
54
|
+
Click a block to insert it on the page.
|
|
55
|
+
</p>
|
|
56
|
+
<div className="flex flex-wrap gap-1.5">
|
|
57
|
+
{categories.map((c) => (
|
|
58
|
+
<button
|
|
59
|
+
key={c}
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => setActive(c)}
|
|
62
|
+
className={cn(
|
|
63
|
+
"rounded-full px-2.5 py-1 text-[11px] font-medium transition",
|
|
64
|
+
active === c
|
|
65
|
+
? "bg-foreground text-background"
|
|
66
|
+
: "bg-muted text-muted-foreground hover:text-foreground",
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{c}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
59
72
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? "bg-secondary font-medium text-secondary-foreground"
|
|
70
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
71
|
-
)}
|
|
72
|
-
>
|
|
73
|
-
{c}
|
|
74
|
-
</button>
|
|
75
|
-
))}
|
|
76
|
-
</nav>
|
|
77
|
-
<div className="grid flex-1 grid-cols-1 content-start gap-4 overflow-auto p-4 sm:grid-cols-2">
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="min-h-0 flex-1 overflow-auto p-3">
|
|
76
|
+
{items.length === 0 ? (
|
|
77
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
78
|
+
No blocks in this category.
|
|
79
|
+
</p>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
78
82
|
{items.flatMap((def) => {
|
|
79
83
|
const variants =
|
|
80
84
|
def.variants && def.variants.length
|
|
81
85
|
? def.variants
|
|
82
|
-
: [{ id: undefined as string | undefined, label:
|
|
86
|
+
: [{ id: undefined as string | undefined, label: "Default" }];
|
|
83
87
|
return variants.map((v) => (
|
|
84
88
|
<button
|
|
85
|
-
key={def.type + (v.id ?? "")}
|
|
89
|
+
key={def.type + (v.id ?? "default")}
|
|
90
|
+
type="button"
|
|
86
91
|
onClick={() => add(def, v.id)}
|
|
87
|
-
className="group overflow-hidden rounded-
|
|
92
|
+
className="group overflow-hidden rounded-xl border border-border/60 bg-muted/40 text-left transition hover:border-foreground/30 hover:shadow-md"
|
|
88
93
|
>
|
|
89
|
-
<div className="relative h-
|
|
94
|
+
<div className="relative h-28 overflow-hidden bg-background">
|
|
90
95
|
<div
|
|
91
96
|
className="pointer-events-none absolute left-0 top-0 origin-top-left"
|
|
92
|
-
style={{
|
|
97
|
+
style={{
|
|
98
|
+
...themeVars(theme),
|
|
99
|
+
width: 960,
|
|
100
|
+
transform: "scale(0.28)",
|
|
101
|
+
}}
|
|
93
102
|
>
|
|
94
103
|
{createElement(def.Component, defaultPropsFor(def, v.id))}
|
|
95
104
|
</div>
|
|
105
|
+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-background/40 to-transparent opacity-0 transition group-hover:opacity-100" />
|
|
96
106
|
</div>
|
|
97
|
-
<div className="border-t px-3 py-2
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
<div className="border-t border-border/40 px-3 py-2">
|
|
108
|
+
<div className="text-xs font-semibold text-foreground">
|
|
109
|
+
{def.label}
|
|
110
|
+
{variants.length > 1 ? (
|
|
111
|
+
<span className="font-normal text-muted-foreground">
|
|
112
|
+
{" "}
|
|
113
|
+
· {v.label}
|
|
114
|
+
</span>
|
|
115
|
+
) : null}
|
|
116
|
+
</div>
|
|
117
|
+
{def.description && (
|
|
118
|
+
<p className="mt-0.5 line-clamp-2 text-[10px] leading-snug text-muted-foreground">
|
|
119
|
+
{def.description}
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
100
122
|
</div>
|
|
101
123
|
</button>
|
|
102
124
|
));
|
|
103
125
|
})}
|
|
104
126
|
</div>
|
|
105
|
-
|
|
127
|
+
)}
|
|
106
128
|
</div>
|
|
107
|
-
</div
|
|
108
|
-
document.body,
|
|
129
|
+
</div>
|
|
109
130
|
);
|
|
110
131
|
}
|
|
132
|
+
|
|
133
|
+
/** @deprecated portal-based library — kept export name for any external import. */
|
|
134
|
+
export function BlockLibrary({ catalog }: { catalog: Catalog }) {
|
|
135
|
+
// Dock hosts BlockLibraryBody; this no-op keeps the export stable.
|
|
136
|
+
void catalog;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single consistent right-side dock for ALL editor panels (sections, design,
|
|
5
|
+
* inspector, block library, host tools). Same position, size, chrome — only
|
|
6
|
+
* the body swaps. Host apps theme via [data-websitekit-editor] tokens.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
import { X } from "lucide-react";
|
|
10
|
+
import { cn } from "../lib/cn";
|
|
11
|
+
|
|
12
|
+
export function EditorDock({
|
|
13
|
+
title,
|
|
14
|
+
subtitle,
|
|
15
|
+
onClose,
|
|
16
|
+
children,
|
|
17
|
+
wide,
|
|
18
|
+
className,
|
|
19
|
+
}: {
|
|
20
|
+
title: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
/** Wider dock for Add Block library (~28rem default, ~36rem wide). */
|
|
25
|
+
wide?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<aside
|
|
30
|
+
data-editor-dock
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex h-full shrink-0 flex-col border-l border-border/50 bg-card/90 text-foreground shadow-[-12px_0_40px_-20px_rgba(0,0,0,0.45)] backdrop-blur-2xl",
|
|
33
|
+
wide ? "w-[min(36rem,100vw)]" : "w-[min(22rem,100vw)]",
|
|
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(78vh,100%)] max-md:w-full max-md:rounded-t-2xl max-md:border-l-0 max-md:border-t max-md:shadow-2xl",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<header className="flex shrink-0 items-start justify-between gap-3 border-b border-border/40 px-4 py-3">
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<h2 className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
41
|
+
{title}
|
|
42
|
+
</h2>
|
|
43
|
+
{subtitle && (
|
|
44
|
+
<p className="mt-0.5 truncate text-sm font-medium text-foreground">{subtitle}</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
{onClose && (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={onClose}
|
|
51
|
+
className="rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
52
|
+
aria-label="Close panel"
|
|
53
|
+
>
|
|
54
|
+
<X className="size-4" />
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
</header>
|
|
58
|
+
<div className="min-h-0 flex-1 overflow-auto">{children}</div>
|
|
59
|
+
</aside>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Thin left tool rail with icon + label so tools are discoverable. */
|
|
64
|
+
export function EditorRail({
|
|
65
|
+
items,
|
|
66
|
+
className,
|
|
67
|
+
}: {
|
|
68
|
+
items: {
|
|
69
|
+
id: string;
|
|
70
|
+
label: string;
|
|
71
|
+
icon: ReactNode;
|
|
72
|
+
active?: boolean;
|
|
73
|
+
onClick: () => void;
|
|
74
|
+
}[];
|
|
75
|
+
className?: string;
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<nav
|
|
79
|
+
data-editor-rail
|
|
80
|
+
aria-label="Editor tools"
|
|
81
|
+
className={cn(
|
|
82
|
+
"z-20 hidden w-[4.25rem] shrink-0 flex-col items-stretch gap-0.5 border-r border-border/40 bg-card/50 py-2 backdrop-blur-xl md:flex",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{items.map((item) => (
|
|
87
|
+
<button
|
|
88
|
+
key={item.id}
|
|
89
|
+
type="button"
|
|
90
|
+
title={item.label}
|
|
91
|
+
onClick={item.onClick}
|
|
92
|
+
className={cn(
|
|
93
|
+
"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
|
+
item.active
|
|
95
|
+
? "bg-foreground text-background shadow-sm"
|
|
96
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<span className="flex size-5 items-center justify-center [&_svg]:size-4">
|
|
100
|
+
{item.icon}
|
|
101
|
+
</span>
|
|
102
|
+
<span className="max-w-full truncate leading-none">{item.label}</span>
|
|
103
|
+
</button>
|
|
104
|
+
))}
|
|
105
|
+
</nav>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* preview-first). Host apps theme via [data-websitekit-editor] + CSS vars.
|
|
4
|
+
* Editor shell — canvas-first with ONE consistent right dock for every panel
|
|
5
|
+
* (sections, design, inspect, add block, host tools). Left rail is labeled.
|
|
7
6
|
*/
|
|
8
7
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
LayoutTemplate,
|
|
10
|
+
List,
|
|
11
|
+
Palette,
|
|
12
|
+
PanelRight,
|
|
13
|
+
Plus,
|
|
14
|
+
Sliders,
|
|
15
|
+
} from "lucide-react";
|
|
10
16
|
import type { PageDocument } from "../model";
|
|
11
17
|
import type { Catalog } from "../registry";
|
|
12
18
|
import { BuilderProvider, useBuilder, useCreateBuilderStore } from "../store";
|
|
13
19
|
import { EditorCapsContext } from "./edit-primitives";
|
|
14
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
EditorUiContext,
|
|
22
|
+
type Device,
|
|
23
|
+
type EditorUi,
|
|
24
|
+
type MobilePanel,
|
|
25
|
+
useEditorUi,
|
|
26
|
+
} from "./ui-context";
|
|
15
27
|
import { Toolbar } from "./Toolbar";
|
|
16
28
|
import { Canvas } from "./Canvas";
|
|
17
29
|
import { Inspector } from "./Inspector";
|
|
18
|
-
import {
|
|
30
|
+
import { BlockLibraryBody } from "./BlockLibrary";
|
|
19
31
|
import { ThemePanel } from "./ThemePanel";
|
|
20
32
|
import { SectionsPanel } from "./SectionsPanel";
|
|
21
33
|
import { TemplateGallery, type TemplateMeta } from "./TemplateGallery";
|
|
22
34
|
import { useEditorShortcuts } from "./shortcuts";
|
|
23
|
-
import {
|
|
35
|
+
import { EditorDock, EditorRail } from "./EditorDock";
|
|
36
|
+
|
|
37
|
+
export type DockMode =
|
|
38
|
+
| "sections"
|
|
39
|
+
| "design"
|
|
40
|
+
| "inspector"
|
|
41
|
+
| "library"
|
|
42
|
+
| "host"
|
|
43
|
+
| null;
|
|
44
|
+
|
|
45
|
+
export interface HostRailItem {
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
icon: ReactNode;
|
|
49
|
+
active?: boolean;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
}
|
|
24
52
|
|
|
25
53
|
export interface PageBuilderProps {
|
|
26
54
|
catalog: Catalog;
|
|
@@ -31,15 +59,11 @@ export interface PageBuilderProps {
|
|
|
31
59
|
templates?: TemplateMeta[];
|
|
32
60
|
saving?: "idle" | "saving" | "saved";
|
|
33
61
|
leading?: ReactNode;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
62
|
+
/** Host tools in the labeled left rail (SEO, Domain, …). */
|
|
63
|
+
hostRail?: HostRailItem[];
|
|
64
|
+
/** When set, dock shows this host body (same right dock as all other panels). */
|
|
65
|
+
hostDock?: { title: string; subtitle?: string; body: ReactNode } | null;
|
|
38
66
|
viewUrl?: string;
|
|
39
|
-
/**
|
|
40
|
-
* floating (default) = max canvas, panels on-call.
|
|
41
|
-
* classic = permanent 3-column rails.
|
|
42
|
-
*/
|
|
43
67
|
layout?: "floating" | "classic";
|
|
44
68
|
}
|
|
45
69
|
|
|
@@ -55,8 +79,6 @@ export function PageBuilder(props: PageBuilderProps) {
|
|
|
55
79
|
);
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
type LeftPanel = "sections" | "design" | null;
|
|
59
|
-
|
|
60
82
|
function PageBuilderInner({
|
|
61
83
|
catalog,
|
|
62
84
|
onChange,
|
|
@@ -65,31 +87,53 @@ function PageBuilderInner({
|
|
|
65
87
|
saving,
|
|
66
88
|
leading,
|
|
67
89
|
viewUrl,
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
hostRail,
|
|
91
|
+
hostDock,
|
|
70
92
|
layout = "floating",
|
|
71
93
|
}: PageBuilderProps) {
|
|
72
94
|
const doc = useBuilder((s) => s.doc);
|
|
73
95
|
const selectedId = useBuilder((s) => s.selectedId);
|
|
96
|
+
const select = useBuilder((s) => s.select);
|
|
74
97
|
const [device, setDevice] = useState<Device>("desktop");
|
|
75
98
|
const [libraryIndex, setLibraryIndex] = useState<number | null>(null);
|
|
76
99
|
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
77
100
|
const [templatesOpen, setTemplatesOpen] = useState(false);
|
|
78
|
-
const [
|
|
101
|
+
const [dock, setDock] = useState<DockMode>("sections");
|
|
79
102
|
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
|
|
80
103
|
|
|
81
|
-
//
|
|
104
|
+
// Library open → force library dock. Close library → restore sections.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (libraryIndex !== null) setDock("library");
|
|
107
|
+
}, [libraryIndex]);
|
|
108
|
+
|
|
109
|
+
// Selecting a section opens inspector in the same dock.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (selectedId && libraryIndex === null && !hostDock) {
|
|
112
|
+
setDock("inspector");
|
|
113
|
+
setInspectorOpen(true);
|
|
114
|
+
}
|
|
115
|
+
}, [selectedId, libraryIndex, hostDock]);
|
|
116
|
+
|
|
117
|
+
// Host dock takes over the unified dock when provided.
|
|
82
118
|
useEffect(() => {
|
|
83
|
-
if (
|
|
84
|
-
|
|
119
|
+
if (hostDock) setDock("host");
|
|
120
|
+
else if (dock === "host") setDock("sections");
|
|
121
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
+
}, [hostDock]);
|
|
85
123
|
|
|
86
124
|
const ui: EditorUi = useMemo(
|
|
87
125
|
() => ({
|
|
88
126
|
device,
|
|
89
127
|
setDevice,
|
|
90
128
|
libraryIndex,
|
|
91
|
-
openLibrary:
|
|
92
|
-
|
|
129
|
+
openLibrary: (index: number | null) => {
|
|
130
|
+
setLibraryIndex(index);
|
|
131
|
+
if (index !== null) setDock("library");
|
|
132
|
+
},
|
|
133
|
+
closeLibrary: () => {
|
|
134
|
+
setLibraryIndex(null);
|
|
135
|
+
setDock((d) => (d === "library" ? "sections" : d));
|
|
136
|
+
},
|
|
93
137
|
inspectorOpen,
|
|
94
138
|
setInspectorOpen,
|
|
95
139
|
templatesOpen,
|
|
@@ -111,295 +155,216 @@ function PageBuilderInner({
|
|
|
111
155
|
onChangeRef.current?.(doc);
|
|
112
156
|
}, [doc]);
|
|
113
157
|
|
|
114
|
-
useEditorShortcuts({
|
|
158
|
+
useEditorShortcuts({
|
|
159
|
+
ui,
|
|
160
|
+
onPublish: onPublish ? () => onPublish(doc) : undefined,
|
|
161
|
+
});
|
|
115
162
|
|
|
116
|
-
const
|
|
117
|
-
|
|
163
|
+
const openDock = (mode: DockMode) => {
|
|
164
|
+
if (mode === "library") {
|
|
165
|
+
ui.openLibrary(doc.sections.length);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Toggle off if same dock re-clicked
|
|
169
|
+
if (mode !== null && mode === dock) {
|
|
170
|
+
setDock(null);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Leaving library
|
|
174
|
+
if (libraryIndex !== null) {
|
|
175
|
+
setLibraryIndex(null);
|
|
176
|
+
}
|
|
177
|
+
setDock(mode);
|
|
178
|
+
if (mode === "inspector") setInspectorOpen(true);
|
|
179
|
+
};
|
|
118
180
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
const closeDock = () => {
|
|
182
|
+
if (libraryIndex !== null) ui.closeLibrary();
|
|
183
|
+
setDock(null);
|
|
184
|
+
select(null);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const railItems = [
|
|
188
|
+
{
|
|
189
|
+
id: "sections",
|
|
190
|
+
label: "Pages",
|
|
191
|
+
icon: <List />,
|
|
192
|
+
active: dock === "sections",
|
|
193
|
+
onClick: () => openDock("sections"),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "add",
|
|
197
|
+
label: "Add",
|
|
198
|
+
icon: <Plus />,
|
|
199
|
+
active: dock === "library",
|
|
200
|
+
onClick: () => openDock("library"),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "design",
|
|
204
|
+
label: "Design",
|
|
205
|
+
icon: <Palette />,
|
|
206
|
+
active: dock === "design",
|
|
207
|
+
onClick: () => openDock("design"),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "inspect",
|
|
211
|
+
label: "Edit",
|
|
212
|
+
icon: <PanelRight />,
|
|
213
|
+
active: dock === "inspector",
|
|
214
|
+
onClick: () => openDock("inspector"),
|
|
215
|
+
},
|
|
216
|
+
...(templates && templates.length
|
|
217
|
+
? [
|
|
218
|
+
{
|
|
219
|
+
id: "templates",
|
|
220
|
+
label: "Looks",
|
|
221
|
+
icon: <LayoutTemplate />,
|
|
222
|
+
active: templatesOpen,
|
|
223
|
+
onClick: () => ui.setTemplatesOpen(true),
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
: []),
|
|
227
|
+
...(hostRail ?? []).map((h) => ({
|
|
228
|
+
id: h.id,
|
|
229
|
+
label: h.label,
|
|
230
|
+
icon: h.icon,
|
|
231
|
+
active: !!h.active || (dock === "host" && hostDock != null),
|
|
232
|
+
onClick: () => {
|
|
233
|
+
h.onClick();
|
|
234
|
+
},
|
|
235
|
+
})),
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const dockTitle =
|
|
239
|
+
dock === "library"
|
|
240
|
+
? "Add block"
|
|
241
|
+
: dock === "design"
|
|
242
|
+
? "Design"
|
|
243
|
+
: dock === "inspector"
|
|
244
|
+
? "Edit section"
|
|
245
|
+
: dock === "host"
|
|
246
|
+
? (hostDock?.title ?? "Site")
|
|
247
|
+
: dock === "sections"
|
|
248
|
+
? "Page structure"
|
|
249
|
+
: "";
|
|
250
|
+
|
|
251
|
+
const dockSubtitle =
|
|
252
|
+
dock === "host"
|
|
253
|
+
? hostDock?.subtitle
|
|
254
|
+
: dock === "inspector"
|
|
255
|
+
? selectedId
|
|
256
|
+
? "Selected block"
|
|
257
|
+
: "Select a block on the page"
|
|
258
|
+
: dock === "library"
|
|
259
|
+
? "Browse layouts"
|
|
260
|
+
: undefined;
|
|
261
|
+
|
|
262
|
+
const showDock = dock !== null;
|
|
161
263
|
|
|
162
|
-
// ── Floating layout (default): max canvas, panels on-call ──────────────
|
|
163
264
|
return (
|
|
164
265
|
<EditorUiContext.Provider value={ui}>
|
|
165
266
|
<div
|
|
166
267
|
data-websitekit-editor
|
|
167
|
-
data-layout=
|
|
268
|
+
data-layout={layout}
|
|
168
269
|
className="relative flex h-full min-h-0 flex-col bg-transparent"
|
|
169
270
|
>
|
|
170
271
|
<Toolbar
|
|
171
272
|
onPublish={onPublish ? () => onPublish(doc) : undefined}
|
|
172
273
|
saving={saving}
|
|
173
|
-
designOpen={
|
|
174
|
-
onToggleDesign={() =>
|
|
274
|
+
designOpen={dock === "design"}
|
|
275
|
+
onToggleDesign={() => openDock("design")}
|
|
175
276
|
hasTemplates={!!templates && templates.length > 0}
|
|
176
277
|
leading={leading}
|
|
177
278
|
viewUrl={viewUrl}
|
|
178
279
|
floating
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
sectionsOpen={leftPanel === "sections"}
|
|
182
|
-
onToggleSections={() => toggleLeft("sections")}
|
|
280
|
+
compact
|
|
281
|
+
onAddBlock={() => openDock("library")}
|
|
183
282
|
/>
|
|
184
283
|
|
|
185
284
|
<div className="relative flex min-h-0 flex-1">
|
|
186
|
-
{
|
|
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>
|
|
285
|
+
<EditorRail items={railItems} />
|
|
221
286
|
|
|
222
|
-
{/* Canvas fills everything else */}
|
|
223
287
|
<div className="relative min-w-0 flex-1">
|
|
224
288
|
<Canvas catalog={catalog} />
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
)}
|
|
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
|
|
289
|
+
{/* On-canvas Add Block CTA */}
|
|
290
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-4 z-10 flex justify-center">
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={() => openDock("library")}
|
|
294
|
+
className="pointer-events-auto inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/90 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-foreground shadow-xl backdrop-blur-xl transition hover:bg-card"
|
|
243
295
|
>
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
296
|
+
<Plus className="size-3.5" />
|
|
297
|
+
Add block
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
247
300
|
</div>
|
|
301
|
+
|
|
302
|
+
{showDock && (
|
|
303
|
+
<EditorDock
|
|
304
|
+
title={dockTitle}
|
|
305
|
+
subtitle={dockSubtitle}
|
|
306
|
+
onClose={closeDock}
|
|
307
|
+
wide={dock === "library"}
|
|
308
|
+
>
|
|
309
|
+
{dock === "sections" && <SectionsPanel catalog={catalog} />}
|
|
310
|
+
{dock === "design" && <ThemePanel />}
|
|
311
|
+
{dock === "inspector" && (
|
|
312
|
+
<div className="p-1">
|
|
313
|
+
{selectedId ? (
|
|
314
|
+
<Inspector catalog={catalog} />
|
|
315
|
+
) : (
|
|
316
|
+
<p className="p-4 text-sm text-muted-foreground">
|
|
317
|
+
Click a section on the page to edit its content and layout.
|
|
318
|
+
</p>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
{dock === "library" && <BlockLibraryBody catalog={catalog} />}
|
|
323
|
+
{dock === "host" && hostDock?.body}
|
|
324
|
+
</EditorDock>
|
|
325
|
+
)}
|
|
248
326
|
</div>
|
|
249
327
|
|
|
250
|
-
<MobileBar />
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
328
|
+
<MobileBar onOpenDock={openDock} />
|
|
329
|
+
{templates && templates.length > 0 && (
|
|
330
|
+
<TemplateGallery templates={templates} />
|
|
331
|
+
)}
|
|
332
|
+
<style>{`[data-websitekit-editor] [data-pb-field]:empty::before{content:attr(data-pb-placeholder);opacity:.4;pointer-events:none;}`}</style>
|
|
255
333
|
</div>
|
|
256
334
|
</EditorUiContext.Provider>
|
|
257
335
|
);
|
|
258
336
|
}
|
|
259
337
|
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
onClick,
|
|
263
|
-
title,
|
|
264
|
-
children,
|
|
338
|
+
function MobileBar({
|
|
339
|
+
onOpenDock,
|
|
265
340
|
}: {
|
|
266
|
-
|
|
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;
|
|
341
|
+
onOpenDock: (m: DockMode) => void;
|
|
300
342
|
}) {
|
|
343
|
+
const ui = useEditorUi();
|
|
344
|
+
const sections = useBuilder((s) => s.doc.sections);
|
|
301
345
|
return (
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
312
|
-
{title}
|
|
313
|
-
</span>
|
|
346
|
+
<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">
|
|
347
|
+
{(
|
|
348
|
+
[
|
|
349
|
+
["sections", "Pages", <List className="size-5" key="l" />],
|
|
350
|
+
["library", "Add", <Plus className="size-5" key="p" />],
|
|
351
|
+
["design", "Design", <Palette className="size-5" key="d" />],
|
|
352
|
+
["inspector", "Edit", <Sliders className="size-5" key="i" />],
|
|
353
|
+
] as const
|
|
354
|
+
).map(([mode, label, icon]) => (
|
|
314
355
|
<button
|
|
356
|
+
key={mode}
|
|
315
357
|
type="button"
|
|
316
|
-
onClick={
|
|
317
|
-
|
|
318
|
-
|
|
358
|
+
onClick={() => {
|
|
359
|
+
if (mode === "library") ui.openLibrary(sections.length);
|
|
360
|
+
else onOpenDock(mode);
|
|
361
|
+
}}
|
|
362
|
+
className="flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] text-muted-foreground"
|
|
319
363
|
>
|
|
320
|
-
|
|
364
|
+
{icon}
|
|
365
|
+
<span>{label}</span>
|
|
321
366
|
</button>
|
|
322
|
-
|
|
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
|
-
|
|
334
|
-
function MobileBar() {
|
|
335
|
-
const ui = useEditorUi();
|
|
336
|
-
const sections = useBuilder((s) => s.doc.sections);
|
|
337
|
-
const tab = (p: MobilePanel, label: string, icon: ReactNode) => (
|
|
338
|
-
<button
|
|
339
|
-
type="button"
|
|
340
|
-
onClick={() => {
|
|
341
|
-
if (p === "inspector") ui.setInspectorOpen(true);
|
|
342
|
-
ui.setMobilePanel(ui.mobilePanel === p ? null : p);
|
|
343
|
-
}}
|
|
344
|
-
className={`flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] ${ui.mobilePanel === p ? "text-foreground" : "text-muted-foreground"}`}
|
|
345
|
-
>
|
|
346
|
-
{icon}
|
|
347
|
-
<span>{label}</span>
|
|
348
|
-
</button>
|
|
349
|
-
);
|
|
350
|
-
return (
|
|
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">
|
|
352
|
-
{tab("sections", "Sections", <List className="size-5" />)}
|
|
353
|
-
<button
|
|
354
|
-
type="button"
|
|
355
|
-
onClick={() => ui.openLibrary(sections.length)}
|
|
356
|
-
className="flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] text-muted-foreground"
|
|
357
|
-
>
|
|
358
|
-
<Plus className="size-5" />
|
|
359
|
-
<span>Add</span>
|
|
360
|
-
</button>
|
|
361
|
-
{tab("design", "Design", <Palette className="size-5" />)}
|
|
362
|
-
{tab("inspector", "Section", <Sliders className="size-5" />)}
|
|
367
|
+
))}
|
|
363
368
|
</nav>
|
|
364
369
|
);
|
|
365
370
|
}
|
|
366
|
-
|
|
367
|
-
function MobileSheet({ catalog }: { catalog: Catalog }) {
|
|
368
|
-
const ui = useEditorUi();
|
|
369
|
-
if (ui.mobilePanel === null) return null;
|
|
370
|
-
return (
|
|
371
|
-
<div className="fixed inset-0 z-50 flex flex-col justify-end md:hidden">
|
|
372
|
-
<button
|
|
373
|
-
aria-label="Close panel"
|
|
374
|
-
onClick={() => ui.setMobilePanel(null)}
|
|
375
|
-
className="absolute inset-0 bg-black/40"
|
|
376
|
-
/>
|
|
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">
|
|
378
|
-
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
379
|
-
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
380
|
-
{ui.mobilePanel === "sections"
|
|
381
|
-
? "Sections"
|
|
382
|
-
: ui.mobilePanel === "design"
|
|
383
|
-
? "Design"
|
|
384
|
-
: "Section"}
|
|
385
|
-
</span>
|
|
386
|
-
<button
|
|
387
|
-
onClick={() => ui.setMobilePanel(null)}
|
|
388
|
-
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
389
|
-
>
|
|
390
|
-
<X className="size-4" />
|
|
391
|
-
</button>
|
|
392
|
-
</div>
|
|
393
|
-
<div className="min-h-[30vh]">
|
|
394
|
-
{ui.mobilePanel === "sections" && <SectionsPanel catalog={catalog} />}
|
|
395
|
-
{ui.mobilePanel === "design" && <ThemePanel />}
|
|
396
|
-
{ui.mobilePanel === "inspector" && (
|
|
397
|
-
<div className="p-4">
|
|
398
|
-
<Inspector catalog={catalog} />
|
|
399
|
-
</div>
|
|
400
|
-
)}
|
|
401
|
-
</div>
|
|
402
|
-
</div>
|
|
403
|
-
</div>
|
|
404
|
-
);
|
|
405
|
-
}
|
package/src/editor/Toolbar.tsx
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Clean editor header: leading · devices · undo · preview · publish. */
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import {
|
|
6
6
|
ExternalLink,
|
|
7
|
-
LayoutGrid,
|
|
8
|
-
List,
|
|
9
7
|
Monitor,
|
|
10
|
-
|
|
11
|
-
PanelRight,
|
|
8
|
+
Plus,
|
|
12
9
|
Redo2,
|
|
13
10
|
Smartphone,
|
|
14
11
|
Tablet,
|
|
@@ -27,30 +24,29 @@ const DEVICES: { id: Device; icon: typeof Monitor; label: string }[] = [
|
|
|
27
24
|
export function Toolbar({
|
|
28
25
|
onPublish,
|
|
29
26
|
saving = "idle",
|
|
30
|
-
designOpen,
|
|
31
|
-
onToggleDesign,
|
|
32
|
-
hasTemplates,
|
|
27
|
+
designOpen: _designOpen,
|
|
28
|
+
onToggleDesign: _onToggleDesign,
|
|
29
|
+
hasTemplates: _hasTemplates,
|
|
33
30
|
leading,
|
|
34
31
|
viewUrl,
|
|
35
32
|
floating,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sectionsOpen,
|
|
39
|
-
onToggleSections,
|
|
33
|
+
compact,
|
|
34
|
+
onAddBlock,
|
|
40
35
|
}: {
|
|
41
36
|
onPublish?: () => void;
|
|
42
37
|
saving?: "idle" | "saving" | "saved";
|
|
43
|
-
designOpen
|
|
44
|
-
onToggleDesign
|
|
38
|
+
designOpen?: boolean;
|
|
39
|
+
onToggleDesign?: () => void;
|
|
45
40
|
hasTemplates?: boolean;
|
|
46
41
|
leading?: ReactNode;
|
|
47
42
|
viewUrl?: string;
|
|
48
43
|
floating?: boolean;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
sectionsOpen?: boolean;
|
|
52
|
-
onToggleSections?: () => void;
|
|
44
|
+
compact?: boolean;
|
|
45
|
+
onAddBlock?: () => void;
|
|
53
46
|
}) {
|
|
47
|
+
void _designOpen;
|
|
48
|
+
void _onToggleDesign;
|
|
49
|
+
void _hasTemplates;
|
|
54
50
|
const ui = useEditorUi();
|
|
55
51
|
const undo = useTemporal((s) => s.undo);
|
|
56
52
|
const redo = useTemporal((s) => s.redo);
|
|
@@ -60,45 +56,18 @@ export function Toolbar({
|
|
|
60
56
|
return (
|
|
61
57
|
<header
|
|
62
58
|
className={cn(
|
|
63
|
-
"relative z-40 flex h-12 shrink-0 items-center gap-
|
|
59
|
+
"relative z-40 flex h-12 shrink-0 items-center gap-2 px-2 text-foreground sm:px-3",
|
|
64
60
|
floating
|
|
65
61
|
? "border-b border-border/40 bg-card/50 backdrop-blur-xl"
|
|
66
62
|
: "border-b border-border bg-card",
|
|
67
63
|
)}
|
|
68
64
|
>
|
|
69
65
|
{leading && (
|
|
70
|
-
<div className="flex min-w-0 items-center gap-1.5
|
|
66
|
+
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
|
|
71
67
|
{leading}
|
|
72
68
|
</div>
|
|
73
69
|
)}
|
|
74
|
-
<div className="flex
|
|
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
|
-
)}
|
|
81
|
-
<ToolButton active={designOpen} onClick={onToggleDesign} title="Design">
|
|
82
|
-
<Palette className="size-4" />
|
|
83
|
-
<span className="hidden text-xs lg:inline">Design</span>
|
|
84
|
-
</ToolButton>
|
|
85
|
-
{hasTemplates && (
|
|
86
|
-
<ToolButton onClick={() => ui.setTemplatesOpen(true)} title="Templates">
|
|
87
|
-
<LayoutGrid className="size-4" />
|
|
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>
|
|
99
|
-
</ToolButton>
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
70
|
+
{!leading && <div className="flex-1" />}
|
|
102
71
|
|
|
103
72
|
<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">
|
|
104
73
|
{DEVICES.map((d) => {
|
|
@@ -121,7 +90,16 @@ export function Toolbar({
|
|
|
121
90
|
})}
|
|
122
91
|
</div>
|
|
123
92
|
|
|
124
|
-
<div className="ml-auto flex items-center gap-0.5">
|
|
93
|
+
<div className="ml-auto flex shrink-0 items-center gap-0.5">
|
|
94
|
+
{onAddBlock && !compact && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onAddBlock}
|
|
98
|
+
className="mr-1 hidden items-center gap-1 rounded-xl border border-border/50 px-2.5 py-1.5 text-xs font-medium text-foreground hover:bg-muted sm:inline-flex"
|
|
99
|
+
>
|
|
100
|
+
<Plus className="size-3.5" /> Add
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
125
103
|
<button
|
|
126
104
|
onClick={() => undo()}
|
|
127
105
|
disabled={!canUndo}
|
|
@@ -143,19 +121,19 @@ export function Toolbar({
|
|
|
143
121
|
href={viewUrl}
|
|
144
122
|
target="_blank"
|
|
145
123
|
rel="noreferrer"
|
|
146
|
-
title="
|
|
147
|
-
className="ml-
|
|
124
|
+
title="Preview live site"
|
|
125
|
+
className="ml-0.5 hidden items-center gap-1 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground sm:flex"
|
|
148
126
|
>
|
|
149
|
-
<ExternalLink className="size-3.5" />
|
|
127
|
+
<ExternalLink className="size-3.5" />
|
|
150
128
|
</a>
|
|
151
129
|
)}
|
|
152
|
-
<span className="mx-1 w-
|
|
153
|
-
{saving === "saving" ? "
|
|
130
|
+
<span className="mx-1 hidden w-10 text-right text-[10px] text-muted-foreground/70 sm:block">
|
|
131
|
+
{saving === "saving" ? "…" : saving === "saved" ? "Saved" : ""}
|
|
154
132
|
</span>
|
|
155
133
|
{onPublish && (
|
|
156
134
|
<button
|
|
157
135
|
onClick={onPublish}
|
|
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
|
|
136
|
+
className="rounded-xl bg-primary px-3.5 py-1.5 text-sm font-semibold text-primary-foreground shadow-sm transition hover:opacity-90"
|
|
159
137
|
>
|
|
160
138
|
Publish
|
|
161
139
|
</button>
|
|
@@ -164,30 +142,3 @@ export function Toolbar({
|
|
|
164
142
|
</header>
|
|
165
143
|
);
|
|
166
144
|
}
|
|
167
|
-
|
|
168
|
-
function ToolButton({
|
|
169
|
-
active,
|
|
170
|
-
onClick,
|
|
171
|
-
title,
|
|
172
|
-
children,
|
|
173
|
-
}: {
|
|
174
|
-
active?: boolean;
|
|
175
|
-
onClick: () => void;
|
|
176
|
-
title: string;
|
|
177
|
-
children: React.ReactNode;
|
|
178
|
-
}) {
|
|
179
|
-
return (
|
|
180
|
-
<button
|
|
181
|
-
onClick={onClick}
|
|
182
|
-
title={title}
|
|
183
|
-
className={cn(
|
|
184
|
-
"flex items-center gap-1.5 rounded-xl px-2 py-1.5 sm:px-2.5",
|
|
185
|
-
active
|
|
186
|
-
? "bg-secondary text-secondary-foreground"
|
|
187
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
188
|
-
)}
|
|
189
|
-
>
|
|
190
|
-
{children}
|
|
191
|
-
</button>
|
|
192
|
-
);
|
|
193
|
-
}
|
package/src/editor/index.tsx
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
/** Client editor entry. Production rendering lives at
|
|
4
4
|
* "@jimmy_harika/websitekit/renderer"; the model/registry at the package root. */
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
PageBuilder,
|
|
7
|
+
type PageBuilderProps,
|
|
8
|
+
type HostRailItem,
|
|
9
|
+
type DockMode,
|
|
10
|
+
} from "./PageBuilder";
|
|
11
|
+
export { EditorDock, EditorRail } from "./EditorDock";
|
|
6
12
|
export { type TemplateMeta } from "./TemplateGallery";
|
|
7
13
|
export { type Device } from "./ui-context";
|
|
8
14
|
export { type EditorCapabilities } from "./edit-primitives";
|