@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jimmy_harika/websitekit",
3
- "version": "0.5.3",
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
- * The "Add section" library (screenshot #3): categories on the left, live mini
5
- * previews of each section + its layout variants on the right. Click inserts at
6
- * the chosen index. Previews render the real catalog Component (scaled), so what
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, useEffect, useState } from "react";
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 BlockLibrary({ catalog }: { catalog: Catalog }) {
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 [active, setActive] = useState(catalog.categories[0] ?? "");
28
- const [mounted, setMounted] = useState(false);
29
- useEffect(() => setMounted(true), []);
24
+ const categories = catalog.categories.length
25
+ ? catalog.categories
26
+ : ["All"];
27
+ const [active, setActive] = useState(categories[0] ?? "All");
30
28
 
31
- if (ui.libraryIndex === null || !mounted) return null;
32
- const items = Object.values(catalog.sections).filter((d) => d.category === active);
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 createPortal(
48
- <div className="fixed inset-0 z-[100] flex">
49
- <div className="flex-1 bg-black/50" onClick={() => ui.closeLibrary()} />
50
- <div className="flex h-full w-full max-w-3xl flex-col bg-card text-foreground shadow-2xl">
51
- <div className="flex items-center justify-between border-b border-border px-5 py-3">
52
- <h2 className="text-sm font-semibold">Add section</h2>
53
- <button
54
- onClick={() => ui.closeLibrary()}
55
- className="rounded p-1 hover:bg-accent"
56
- >
57
- <X className="size-5" />
58
- </button>
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
- <div className="flex min-h-0 flex-1">
61
- <nav className="w-40 shrink-0 overflow-auto border-r p-2">
62
- {catalog.categories.map((c) => (
63
- <button
64
- key={c}
65
- onClick={() => setActive(c)}
66
- className={cn(
67
- "block w-full rounded px-3 py-2 text-left text-sm",
68
- active === c
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: def.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-lg border bg-muted text-left transition hover:border-sky-400 hover:shadow"
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-32 overflow-hidden bg-background">
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={{ ...themeVars(theme), width: 1100, transform: "scale(0.318)" }}
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 text-xs font-medium text-foreground">
98
- {def.label}
99
- {variants.length > 1 ? ` · ${v.label}` : ""}
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
- </div>
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
- * 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.
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 { List, Palette, PanelRight, Plus, Sliders, X } from "lucide-react";
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 { EditorUiContext, type Device, type EditorUi, type MobilePanel, useEditorUi } from "./ui-context";
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 { BlockLibrary } from "./BlockLibrary";
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 { cn } from "../lib/cn";
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
- /** 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;
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
- railExtra,
69
- hostPanel,
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 [leftPanel, setLeftPanel] = useState<LeftPanel>("sections");
101
+ const [dock, setDock] = useState<DockMode>("sections");
79
102
  const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
80
103
 
81
- // Open inspector when a section is selected (floating mode); keep open in classic.
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 (layout === "floating" && selectedId) setInspectorOpen(true);
84
- }, [selectedId, layout]);
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: setLibraryIndex,
92
- closeLibrary: () => setLibraryIndex(null),
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({ ui, onPublish: onPublish ? () => onPublish(doc) : undefined });
158
+ useEditorShortcuts({
159
+ ui,
160
+ onPublish: onPublish ? () => onPublish(doc) : undefined,
161
+ });
115
162
 
116
- const toggleLeft = (p: LeftPanel) =>
117
- setLeftPanel((cur) => (cur === p ? null : p));
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
- 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
- }
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="floating"
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={leftPanel === "design"}
174
- onToggleDesign={() => toggleLeft("design")}
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
- inspectorOpen={inspectorOpen}
180
- onToggleInspector={() => setInspectorOpen((v) => !v)}
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
- {/* 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>
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
- {/* 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>
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
- <Inspector catalog={catalog} />
245
- </FloatingCard>
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
- <MobileSheet catalog={catalog} />
252
- <BlockLibrary catalog={catalog} />
253
- {templates && templates.length > 0 && <TemplateGallery templates={templates} />}
254
- <EditorStyle />
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 RailBtn({
261
- active,
262
- onClick,
263
- title,
264
- children,
338
+ function MobileBar({
339
+ onOpenDock,
265
340
  }: {
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;
341
+ onOpenDock: (m: DockMode) => void;
300
342
  }) {
343
+ const ui = useEditorUi();
344
+ const sections = useBuilder((s) => s.doc.sections);
301
345
  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>
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={onClose}
317
- className="rounded-lg p-1 text-muted-foreground transition hover:bg-muted hover:text-foreground"
318
- aria-label="Close panel"
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
- <X className="size-3.5" />
364
+ {icon}
365
+ <span>{label}</span>
321
366
  </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
-
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
- }
@@ -1,14 +1,11 @@
1
1
  "use client";
2
2
 
3
- /** Editor header glass-friendly surface, device switcher, undo/redo, publish. */
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
- Palette,
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
- inspectorOpen,
37
- onToggleInspector,
38
- sectionsOpen,
39
- onToggleSections,
33
+ compact,
34
+ onAddBlock,
40
35
  }: {
41
36
  onPublish?: () => void;
42
37
  saving?: "idle" | "saving" | "saved";
43
- designOpen: boolean;
44
- onToggleDesign: () => void;
38
+ designOpen?: boolean;
39
+ onToggleDesign?: () => void;
45
40
  hasTemplates?: boolean;
46
41
  leading?: ReactNode;
47
42
  viewUrl?: string;
48
43
  floating?: boolean;
49
- inspectorOpen?: boolean;
50
- onToggleInspector?: () => void;
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-1 px-2 text-foreground sm:px-3",
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 pr-1.5 sm:border-r sm:border-border/40">
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 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
- )}
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="View live site"
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"
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" /> Preview
127
+ <ExternalLink className="size-3.5" />
150
128
  </a>
151
129
  )}
152
- <span className="mx-1 w-12 text-right text-[11px] text-muted-foreground/70">
153
- {saving === "saving" ? "Saving…" : saving === "saved" ? "Saved" : ""}
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 sm:px-4"
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
- }
@@ -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 { PageBuilder, type PageBuilderProps } from "./PageBuilder";
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";