@jimmy_harika/websitekit 0.5.2 → 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.2",
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
+ }
@@ -74,7 +74,7 @@ export function Canvas({ catalog }: { catalog: Catalog }) {
74
74
 
75
75
  return (
76
76
  <div
77
- className="flex-1 overflow-auto bg-muted p-4 sm:p-6"
77
+ className="flex-1 overflow-auto bg-transparent p-3 sm:p-5 md:p-6"
78
78
  onClick={() => select(null)}
79
79
  >
80
80
  <div
@@ -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,42 +1,70 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * Top-level editor. The app wires it with a `catalog`, an `initialDocument`,
5
- * and capability callbacks (autosave via `onChange`, `onPublish`, `uploadImage`,
6
- * optional `templates`). Owns the store, editor UI state, and the layout:
7
- * top toolbar · [design panel] · canvas · [inspector] · overlays (library,
8
- * templates).
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.
9
6
  */
10
7
  import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
11
- import { List, Palette, Plus, Sliders, X } from "lucide-react";
8
+ import {
9
+ LayoutTemplate,
10
+ List,
11
+ Palette,
12
+ PanelRight,
13
+ Plus,
14
+ Sliders,
15
+ } from "lucide-react";
12
16
  import type { PageDocument } from "../model";
13
17
  import type { Catalog } from "../registry";
14
18
  import { BuilderProvider, useBuilder, useCreateBuilderStore } from "../store";
15
19
  import { EditorCapsContext } from "./edit-primitives";
16
- 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";
17
27
  import { Toolbar } from "./Toolbar";
18
28
  import { Canvas } from "./Canvas";
19
29
  import { Inspector } from "./Inspector";
20
- import { BlockLibrary } from "./BlockLibrary";
30
+ import { BlockLibraryBody } from "./BlockLibrary";
21
31
  import { ThemePanel } from "./ThemePanel";
22
32
  import { SectionsPanel } from "./SectionsPanel";
23
33
  import { TemplateGallery, type TemplateMeta } from "./TemplateGallery";
24
34
  import { useEditorShortcuts } from "./shortcuts";
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
+ }
25
52
 
26
53
  export interface PageBuilderProps {
27
54
  catalog: Catalog;
28
55
  initialDocument: PageDocument;
29
- /** Fires (debounced upstream) whenever the document changes — for autosave. */
30
56
  onChange?: (doc: PageDocument) => void;
31
57
  onPublish?: (doc: PageDocument) => void | Promise<void>;
32
- /** Upload a picked file, return its public URL (R2 / Convex storage). */
33
58
  uploadImage?: (file: File) => Promise<string>;
34
59
  templates?: TemplateMeta[];
35
60
  saving?: "idle" | "saving" | "saved";
36
- /** Header gate content (e.g. exit link + site name), pinned far left. */
37
61
  leading?: ReactNode;
38
- /** "View live site" link target. */
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;
39
66
  viewUrl?: string;
67
+ layout?: "floating" | "classic";
40
68
  }
41
69
 
42
70
  export function PageBuilder(props: PageBuilderProps) {
@@ -59,22 +87,53 @@ function PageBuilderInner({
59
87
  saving,
60
88
  leading,
61
89
  viewUrl,
90
+ hostRail,
91
+ hostDock,
92
+ layout = "floating",
62
93
  }: PageBuilderProps) {
63
94
  const doc = useBuilder((s) => s.doc);
95
+ const selectedId = useBuilder((s) => s.selectedId);
96
+ const select = useBuilder((s) => s.select);
64
97
  const [device, setDevice] = useState<Device>("desktop");
65
98
  const [libraryIndex, setLibraryIndex] = useState<number | null>(null);
66
- const [inspectorOpen, setInspectorOpen] = useState(true);
99
+ const [inspectorOpen, setInspectorOpen] = useState(false);
67
100
  const [templatesOpen, setTemplatesOpen] = useState(false);
68
- const [designOpen, setDesignOpen] = useState(false);
101
+ const [dock, setDock] = useState<DockMode>("sections");
69
102
  const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
70
103
 
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.
118
+ useEffect(() => {
119
+ if (hostDock) setDock("host");
120
+ else if (dock === "host") setDock("sections");
121
+ // eslint-disable-next-line react-hooks/exhaustive-deps
122
+ }, [hostDock]);
123
+
71
124
  const ui: EditorUi = useMemo(
72
125
  () => ({
73
126
  device,
74
127
  setDevice,
75
128
  libraryIndex,
76
- openLibrary: setLibraryIndex,
77
- 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
+ },
78
137
  inspectorOpen,
79
138
  setInspectorOpen,
80
139
  templatesOpen,
@@ -85,7 +144,6 @@ function PageBuilderInner({
85
144
  [device, libraryIndex, inspectorOpen, templatesOpen, mobilePanel],
86
145
  );
87
146
 
88
- // Autosave: emit document changes (skip the initial render).
89
147
  const onChangeRef = useRef(onChange);
90
148
  onChangeRef.current = onChange;
91
149
  const firstRender = useRef(true);
@@ -97,121 +155,216 @@ function PageBuilderInner({
97
155
  onChangeRef.current?.(doc);
98
156
  }, [doc]);
99
157
 
100
- // Keyboard shortcuts (⌘Z/⌘⇧Z, Delete, ⌘D, Esc, ⌘/, ⌘⇧P, ↑/↓).
101
- // `ui` is passed in (not read from context) because this hook runs in
102
- // PageBuilderInner, which sits ABOVE its own <EditorUiContext.Provider>.
103
- useEditorShortcuts({ ui, onPublish: onPublish ? () => onPublish(doc) : undefined });
158
+ useEditorShortcuts({
159
+ ui,
160
+ onPublish: onPublish ? () => onPublish(doc) : undefined,
161
+ });
162
+
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
+ };
180
+
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;
104
263
 
105
264
  return (
106
265
  <EditorUiContext.Provider value={ui}>
107
- <div data-websitekit-editor className="flex h-full min-h-0 flex-col bg-muted">
266
+ <div
267
+ data-websitekit-editor
268
+ data-layout={layout}
269
+ className="relative flex h-full min-h-0 flex-col bg-transparent"
270
+ >
108
271
  <Toolbar
109
272
  onPublish={onPublish ? () => onPublish(doc) : undefined}
110
273
  saving={saving}
111
- designOpen={designOpen}
112
- onToggleDesign={() => setDesignOpen((v) => !v)}
274
+ designOpen={dock === "design"}
275
+ onToggleDesign={() => openDock("design")}
113
276
  hasTemplates={!!templates && templates.length > 0}
114
277
  leading={leading}
115
278
  viewUrl={viewUrl}
279
+ floating
280
+ compact
281
+ onAddBlock={() => openDock("library")}
116
282
  />
117
- <div className="flex min-h-0 flex-1">
118
- <aside className="hidden w-64 shrink-0 flex-col border-r border-border bg-card md:flex">
119
- {designOpen ? (
120
- <>
121
- <div className="border-b border-border px-3 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
122
- Design
123
- </div>
124
- <div className="min-h-0 flex-1 overflow-auto">
125
- <ThemePanel />
283
+
284
+ <div className="relative flex min-h-0 flex-1">
285
+ <EditorRail items={railItems} />
286
+
287
+ <div className="relative min-w-0 flex-1">
288
+ <Canvas catalog={catalog} />
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"
295
+ >
296
+ <Plus className="size-3.5" />
297
+ Add block
298
+ </button>
299
+ </div>
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
+ )}
126
320
  </div>
127
- </>
128
- ) : (
129
- <SectionsPanel catalog={catalog} />
130
- )}
131
- </aside>
132
- <Canvas catalog={catalog} />
133
- {/* Desktop inspector rail */}
134
- <aside className="hidden w-80 shrink-0 flex-col border-l border-border bg-card md:flex">
135
- <Inspector catalog={catalog} />
136
- </aside>
321
+ )}
322
+ {dock === "library" && <BlockLibraryBody catalog={catalog} />}
323
+ {dock === "host" && hostDock?.body}
324
+ </EditorDock>
325
+ )}
137
326
  </div>
138
- <MobileBar />
139
- <MobileSheet catalog={catalog} designOpen={designOpen} />
140
- <BlockLibrary catalog={catalog} />
141
- {templates && templates.length > 0 && <TemplateGallery templates={templates} />}
327
+
328
+ <MobileBar onOpenDock={openDock} />
329
+ {templates && templates.length > 0 && (
330
+ <TemplateGallery templates={templates} />
331
+ )}
142
332
  <style>{`[data-websitekit-editor] [data-pb-field]:empty::before{content:attr(data-pb-placeholder);opacity:.4;pointer-events:none;}`}</style>
143
333
  </div>
144
334
  </EditorUiContext.Provider>
145
335
  );
146
336
  }
147
337
 
148
- /** Mobile bottom tab bar — the only way to reach panels on < md. */
149
- function MobileBar() {
338
+ function MobileBar({
339
+ onOpenDock,
340
+ }: {
341
+ onOpenDock: (m: DockMode) => void;
342
+ }) {
150
343
  const ui = useEditorUi();
151
344
  const sections = useBuilder((s) => s.doc.sections);
152
- const tab = (p: MobilePanel, label: string, icon: ReactNode) => (
153
- <button
154
- type="button"
155
- onClick={() => {
156
- if (p === "inspector") ui.setInspectorOpen(true);
157
- ui.setMobilePanel(ui.mobilePanel === p ? null : p);
158
- }}
159
- className={`flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] ${ui.mobilePanel === p ? "text-foreground" : "text-muted-foreground"}`}
160
- >
161
- {icon}
162
- <span>{label}</span>
163
- </button>
164
- );
165
345
  return (
166
- <nav className="flex shrink-0 items-stretch border-t border-border bg-card px-1 pb-[env(safe-area-inset-bottom)] md:hidden">
167
- {tab("sections", "Sections", <List className="size-5" />)}
168
- <button
169
- type="button"
170
- onClick={() => ui.openLibrary(sections.length)}
171
- className="flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] text-muted-foreground"
172
- >
173
- <Plus className="size-5" />
174
- <span>Add</span>
175
- </button>
176
- {tab("design", "Design", <Palette className="size-5" />)}
177
- {tab("inspector", "Section", <Sliders className="size-5" />)}
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]) => (
355
+ <button
356
+ key={mode}
357
+ type="button"
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"
363
+ >
364
+ {icon}
365
+ <span>{label}</span>
366
+ </button>
367
+ ))}
178
368
  </nav>
179
369
  );
180
370
  }
181
-
182
- /** Bottom-sheet overlay rendering the active mobile panel. */
183
- function MobileSheet({ catalog, designOpen }: { catalog: Catalog; designOpen: boolean }) {
184
- const ui = useEditorUi();
185
- if (ui.mobilePanel === null) return null;
186
- return (
187
- <div className="fixed inset-0 z-50 flex flex-col justify-end md:hidden">
188
- <button
189
- aria-label="Close panel"
190
- onClick={() => ui.setMobilePanel(null)}
191
- className="absolute inset-0 bg-black/40"
192
- />
193
- <div className="relative max-h-[70vh] overflow-auto rounded-t-2xl border-t border-border bg-card pb-[env(safe-area-inset-bottom)]">
194
- <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
195
- <span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
196
- {ui.mobilePanel === "sections" ? "Sections" : ui.mobilePanel === "design" ? "Design" : "Section"}
197
- </span>
198
- <button
199
- onClick={() => ui.setMobilePanel(null)}
200
- className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
201
- >
202
- <X className="size-4" />
203
- </button>
204
- </div>
205
- <div className="min-h-[30vh]">
206
- {ui.mobilePanel === "sections" && <SectionsPanel catalog={catalog} />}
207
- {ui.mobilePanel === "design" && <ThemePanel />}
208
- {ui.mobilePanel === "inspector" && (
209
- <div className="p-4">
210
- <Inspector catalog={catalog} />
211
- </div>
212
- )}
213
- </div>
214
- </div>
215
- </div>
216
- );
217
- }
@@ -1,14 +1,11 @@
1
1
  "use client";
2
2
 
3
- /** Single cohesive editor header: gate (exit + site name) · design · templates ·
4
- * device preview · undo/redo · view · save status · publish. Theme-optimized
5
- * (bg-card surface, semantic tokens). */
3
+ /** Clean editor header: leading · devices · undo · preview · publish. */
6
4
  import type { ReactNode } from "react";
7
5
  import {
8
6
  ExternalLink,
9
- LayoutGrid,
10
7
  Monitor,
11
- Palette,
8
+ Plus,
12
9
  Redo2,
13
10
  Smartphone,
14
11
  Tablet,
@@ -27,22 +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,
32
+ floating,
33
+ compact,
34
+ onAddBlock,
35
35
  }: {
36
36
  onPublish?: () => void;
37
37
  saving?: "idle" | "saving" | "saved";
38
- designOpen: boolean;
39
- onToggleDesign: () => void;
38
+ designOpen?: boolean;
39
+ onToggleDesign?: () => void;
40
40
  hasTemplates?: boolean;
41
- /** Gate content (exit + site name), pinned far left. */
42
41
  leading?: ReactNode;
43
- /** "View live site" link target. */
44
42
  viewUrl?: string;
43
+ floating?: boolean;
44
+ compact?: boolean;
45
+ onAddBlock?: () => void;
45
46
  }) {
47
+ void _designOpen;
48
+ void _onToggleDesign;
49
+ void _hasTemplates;
46
50
  const ui = useEditorUi();
47
51
  const undo = useTemporal((s) => s.undo);
48
52
  const redo = useTemporal((s) => s.redo);
@@ -50,26 +54,22 @@ export function Toolbar({
50
54
  const canRedo = useTemporal((s) => s.futureStates.length > 0);
51
55
 
52
56
  return (
53
- <header className="relative z-40 flex h-12 shrink-0 items-center gap-1 border-b border-border bg-card px-2 text-foreground sm:px-3">
57
+ <header
58
+ className={cn(
59
+ "relative z-40 flex h-12 shrink-0 items-center gap-2 px-2 text-foreground sm:px-3",
60
+ floating
61
+ ? "border-b border-border/40 bg-card/50 backdrop-blur-xl"
62
+ : "border-b border-border bg-card",
63
+ )}
64
+ >
54
65
  {leading && (
55
- <div className="flex min-w-0 items-center gap-1.5 pr-1.5 sm:border-r sm:border-border">
66
+ <div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
56
67
  {leading}
57
68
  </div>
58
69
  )}
59
- <div className="flex items-center gap-0.5 pl-0.5">
60
- <ToolButton active={designOpen} onClick={onToggleDesign} title="Design">
61
- <Palette className="size-4" />
62
- <span className="hidden text-xs md:inline">Design</span>
63
- </ToolButton>
64
- {hasTemplates && (
65
- <ToolButton onClick={() => ui.setTemplatesOpen(true)} title="Templates">
66
- <LayoutGrid className="size-4" />
67
- <span className="hidden text-xs md:inline">Templates</span>
68
- </ToolButton>
69
- )}
70
- </div>
70
+ {!leading && <div className="flex-1" />}
71
71
 
72
- <div className="absolute left-1/2 flex -translate-x-1/2 items-center rounded-lg border border-border bg-background p-0.5">
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">
73
73
  {DEVICES.map((d) => {
74
74
  const Icon = d.icon;
75
75
  return (
@@ -78,9 +78,9 @@ export function Toolbar({
78
78
  title={d.label}
79
79
  onClick={() => ui.setDevice(d.id)}
80
80
  className={cn(
81
- "rounded-md p-1.5 transition",
81
+ "rounded-lg p-1.5 transition",
82
82
  ui.device === d.id
83
- ? "bg-secondary text-secondary-foreground"
83
+ ? "bg-secondary text-secondary-foreground shadow-sm"
84
84
  : "text-muted-foreground hover:bg-muted",
85
85
  )}
86
86
  >
@@ -90,12 +90,21 @@ export function Toolbar({
90
90
  })}
91
91
  </div>
92
92
 
93
- <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
+ )}
94
103
  <button
95
104
  onClick={() => undo()}
96
105
  disabled={!canUndo}
97
106
  title="Undo"
98
- className="rounded-md p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
107
+ className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
99
108
  >
100
109
  <Undo2 className="size-4" />
101
110
  </button>
@@ -103,7 +112,7 @@ export function Toolbar({
103
112
  onClick={() => redo()}
104
113
  disabled={!canRedo}
105
114
  title="Redo"
106
- className="rounded-md p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
115
+ className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
107
116
  >
108
117
  <Redo2 className="size-4" />
109
118
  </button>
@@ -112,19 +121,19 @@ export function Toolbar({
112
121
  href={viewUrl}
113
122
  target="_blank"
114
123
  rel="noreferrer"
115
- title="View live site"
116
- className="ml-1 hidden items-center gap-1 rounded-md 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"
117
126
  >
118
- <ExternalLink className="size-3.5" /> View
127
+ <ExternalLink className="size-3.5" />
119
128
  </a>
120
129
  )}
121
- <span className="mx-1 w-12 text-right text-[11px] text-muted-foreground/70">
122
- {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" : ""}
123
132
  </span>
124
133
  {onPublish && (
125
134
  <button
126
135
  onClick={onPublish}
127
- className="rounded-lg bg-primary px-3.5 py-1.5 text-sm font-medium 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"
128
137
  >
129
138
  Publish
130
139
  </button>
@@ -133,30 +142,3 @@ export function Toolbar({
133
142
  </header>
134
143
  );
135
144
  }
136
-
137
- function ToolButton({
138
- active,
139
- onClick,
140
- title,
141
- children,
142
- }: {
143
- active?: boolean;
144
- onClick: () => void;
145
- title: string;
146
- children: React.ReactNode;
147
- }) {
148
- return (
149
- <button
150
- onClick={onClick}
151
- title={title}
152
- className={cn(
153
- "flex items-center gap-1.5 rounded-md px-2 py-1.5 sm:px-2.5",
154
- active
155
- ? "bg-secondary text-secondary-foreground"
156
- : "text-muted-foreground hover:bg-muted hover:text-foreground",
157
- )}
158
- >
159
- {children}
160
- </button>
161
- );
162
- }
@@ -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";