@jimmy_harika/websitekit 0.5.3 → 0.5.5

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.5",
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
+ }
@@ -89,7 +89,7 @@ export function Canvas({ catalog }: { catalog: Catalog }) {
89
89
  e.stopPropagation();
90
90
  ui.openLibrary(0);
91
91
  }}
92
- className="flex min-h-[60vh] w-full flex-col items-center justify-center gap-3 text-muted-foreground/70 transition hover:text-sky-500"
92
+ className="flex min-h-[60vh] w-full flex-col items-center justify-center gap-3 text-muted-foreground/70 transition hover:text-primary"
93
93
  >
94
94
  <span className="flex size-12 items-center justify-center rounded-full border-2 border-dashed border-current">
95
95
  <Plus className="size-6" />
@@ -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-none 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 bg-card/80 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-primary text-primary-foreground 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
+ }
@@ -12,19 +12,19 @@ export function InsertPoint({ index }: { index: number }) {
12
12
  {/* Always visible on touch/small screens (group-hover doesn't fire on touch);
13
13
  hover-reveal on desktop. Without this a mobile user can't add a 2nd section. */}
14
14
  <div className="pointer-events-none absolute inset-x-0 -top-4 flex h-8 items-center justify-center px-6 opacity-100 transition group-hover/ins:opacity-100 md:opacity-0 md:group-hover/ins:opacity-100">
15
- <div className="h-px flex-1 bg-sky-400/70" />
15
+ <div className="h-px flex-1 bg-primary/70" />
16
16
  <button
17
17
  type="button"
18
18
  onClick={(e) => {
19
19
  e.stopPropagation();
20
20
  ui.openLibrary(index);
21
21
  }}
22
- className="pointer-events-auto mx-2 flex size-7 items-center justify-center rounded-full bg-sky-500 text-white shadow-md transition hover:scale-105 hover:bg-sky-600"
22
+ className="pointer-events-auto mx-2 flex size-7 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-md transition hover:scale-105 hover:bg-primary/90"
23
23
  title="Add section here"
24
24
  >
25
25
  <Plus className="size-4" />
26
26
  </button>
27
- <div className="h-px flex-1 bg-sky-400/70" />
27
+ <div className="h-px flex-1 bg-primary/70" />
28
28
  </div>
29
29
  </div>
30
30
  );
@@ -17,7 +17,14 @@ const BP: { id: Breakpoint; icon: typeof Monitor; label: string }[] = [
17
17
  { id: "desktop", icon: Monitor, label: "Desktop" },
18
18
  ];
19
19
 
20
- export function Inspector({ catalog }: { catalog: Catalog }) {
20
+ export function Inspector({
21
+ catalog,
22
+ /** When true (docked mode), skip the outer chrome — parent dock owns the title bar. */
23
+ embedded,
24
+ }: {
25
+ catalog: Catalog;
26
+ embedded?: boolean;
27
+ }) {
21
28
  const ui = useEditorUi();
22
29
  const selectedId = useBuilder((s) => s.selectedId);
23
30
  const section = useBuilder(
@@ -29,7 +36,9 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
29
36
  const remove = useBuilder((s) => s.removeSection);
30
37
  const duplicate = useBuilder((s) => s.duplicateSection);
31
38
 
32
- if (!ui.inspectorOpen || !section) return null;
39
+ // In embedded/dock mode the parent already decided to show us — don't require inspectorOpen.
40
+ if (!embedded && !ui.inspectorOpen) return null;
41
+ if (!section) return null;
33
42
  const def = getDefinition(catalog, section.type);
34
43
  if (!def) return null;
35
44
 
@@ -43,18 +52,27 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
43
52
 
44
53
  return (
45
54
  <div className="flex h-full min-h-0 flex-col">
46
- <div className="flex items-center justify-between border-b px-4 py-3">
47
- <div>
48
- <p className="text-[11px] uppercase tracking-wider text-muted-foreground/70">Section</p>
49
- <h3 className="text-sm font-semibold">{def.label}</h3>
55
+ {!embedded && (
56
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
57
+ <div>
58
+ <p className="text-[11px] uppercase tracking-wider text-muted-foreground">
59
+ Section
60
+ </p>
61
+ <h3 className="text-sm font-semibold text-foreground">{def.label}</h3>
62
+ </div>
63
+ <button
64
+ onClick={() => ui.setInspectorOpen(false)}
65
+ className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
66
+ >
67
+ <X className="size-4" />
68
+ </button>
50
69
  </div>
51
- <button
52
- onClick={() => ui.setInspectorOpen(false)}
53
- className="rounded p-1 hover:bg-accent"
54
- >
55
- <X className="size-4" />
56
- </button>
57
- </div>
70
+ )}
71
+ {embedded && (
72
+ <div className="border-b border-border px-4 py-2.5">
73
+ <p className="text-sm font-semibold text-foreground">{def.label}</p>
74
+ </div>
75
+ )}
58
76
  <div className="flex-1 space-y-5 overflow-auto p-4">
59
77
  <div>
60
78
  <label className="mb-1.5 block text-xs font-medium text-muted-foreground">
@@ -137,7 +155,7 @@ export function Inspector({ catalog }: { catalog: Catalog }) {
137
155
  </button>
138
156
  <button
139
157
  onClick={() => remove(section.id)}
140
- className="flex-1 rounded-md border border-red-200 px-3 py-2 text-xs text-red-600 hover:bg-red-50"
158
+ className="flex-1 rounded-md border border-destructive/30 px-3 py-2 text-xs text-destructive hover:bg-destructive/10"
141
159
  >
142
160
  Delete
143
161
  </button>
@@ -328,7 +346,7 @@ function ImageField({
328
346
  <img
329
347
  src={value}
330
348
  alt={altValue ?? ""}
331
- className={`h-24 w-full rounded-md object-cover ${dragOver ? "ring-2 ring-sky-400" : ""}`}
349
+ className={`h-24 w-full rounded-md object-cover ${dragOver ? "ring-2 ring-primary" : ""}`}
332
350
  onDragOver={(e) => {
333
351
  e.preventDefault();
334
352
  setDragOver(true);
@@ -344,7 +362,7 @@ function ImageField({
344
362
  ) : (
345
363
  <div
346
364
  className={`flex h-24 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground/70 ${
347
- dragOver ? "border-sky-400 bg-sky-400/10" : ""
365
+ dragOver ? "border-primary bg-primary/10" : ""
348
366
  }`}
349
367
  onDragOver={(e) => {
350
368
  e.preventDefault();
@@ -377,7 +395,7 @@ function ImageField({
377
395
  </button>
378
396
  )}
379
397
  </div>
380
- {err && <p className="text-[10px] text-red-600 dark:text-red-400">{err}</p>}
398
+ {err && <p className="text-[10px] text-destructive">{err}</p>}
381
399
  <input ref={ref} type="file" accept="image/*,.heic,.heif,image/heic,image/heif" hidden onChange={pick} />
382
400
  <input
383
401
  value={value}
@@ -482,7 +500,7 @@ function ImageListField({
482
500
  <div key={`${url}-${i}`} className="group relative">
483
501
  {/* eslint-disable-next-line @next/next/no-img-element */}
484
502
  <img src={url} alt="" className="aspect-square w-full rounded-md object-cover" />
485
- <div className="absolute inset-0 hidden items-end justify-between rounded-md bg-black/40 p-1 group-hover:flex">
503
+ <div className="absolute inset-0 hidden items-end justify-between rounded-md bg-foreground/40 p-1 group-hover:flex">
486
504
  <div className="flex gap-0.5">
487
505
  <button
488
506
  title="Move left"
@@ -515,7 +533,7 @@ function ImageListField({
515
533
  ) : (
516
534
  <div
517
535
  className={`flex h-16 items-center justify-center rounded-md border border-dashed text-xs text-muted-foreground/70 ${
518
- dragOver ? "border-sky-400 bg-sky-400/10" : ""
536
+ dragOver ? "border-primary bg-primary/10" : ""
519
537
  }`}
520
538
  onDragOver={(e) => {
521
539
  e.preventDefault();
@@ -535,7 +553,7 @@ function ImageListField({
535
553
  onClick={() => ref.current?.click()}
536
554
  disabled={busy || room === 0}
537
555
  className={`w-full rounded-md border px-2 py-1.5 text-xs hover:bg-muted disabled:opacity-50 ${
538
- dragOver ? "border-sky-400" : ""
556
+ dragOver ? "border-primary" : ""
539
557
  }`}
540
558
  onDragOver={(e) => {
541
559
  e.preventDefault();
@@ -670,7 +688,7 @@ function ItemListField({
670
688
  <button
671
689
  title="Remove"
672
690
  onClick={() => onChange(value.filter((_, j) => j !== i))}
673
- className="rounded border px-1 text-[10px] leading-4 text-red-600"
691
+ className="rounded border px-1 text-[10px] leading-4 text-destructive"
674
692
  >
675
693
  ×
676
694
  </button>