@morphika/andami 0.4.0 → 0.4.2
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/README.md +2 -1
- package/components/blocks/BlockRenderer.tsx +21 -46
- package/components/builder/BlockLivePreview.tsx +18 -47
- package/components/builder/ColumnDragOverlay.tsx +21 -6
- package/components/builder/hooks/useColumnDrag.ts +64 -11
- package/components/builder/settings-panel/BlockSettings.tsx +18 -70
- package/lib/builder/block-registrations.ts +95 -28
- package/lib/builder/defaults.ts +22 -102
- package/lib/builder/store-blocks.ts +2 -2
- package/lib/builder/store-canvas.ts +2 -2
- package/lib/builder/store-cover.ts +2 -2
- package/lib/builder/store-helpers.ts +43 -0
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +387 -0
- package/lib/builder/types.ts +77 -225
- package/lib/version.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A reusable Visual Page Builder framework for Next.js. Build custom websites with
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Visual Page Builder** — Infinite canvas editor with device previews (desktop, tablet, phone)
|
|
10
|
-
- **
|
|
10
|
+
- **6 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button (added via "+ Add Block" inside columns)
|
|
11
|
+
- **2 Section-level Blocks** — Project Grid (masonry) and Project Carousel (horizontal "keep browsing" at end of project pages). Added via "+ Add Section"
|
|
11
12
|
- **Cover Sections** — Full-viewport hero sections with proportional rows, background media, and drag-to-resize
|
|
12
13
|
- **V2 Grid System** — 12-column CSS grid with push cascade engine and responsive overrides
|
|
13
14
|
- **Custom Sections** — Create reusable sections with per-instance setting overrides
|
|
@@ -17,14 +17,8 @@ import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
|
17
17
|
import HoverAnimationWrapper from "./HoverAnimationWrapper";
|
|
18
18
|
import TypewriterWrapper from "./TypewriterWrapper";
|
|
19
19
|
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import ImageGridBlockRenderer from "./ImageGridBlockRenderer";
|
|
23
|
-
import VideoBlockRenderer from "./VideoBlockRenderer";
|
|
24
|
-
import SpacerBlockRenderer from "./SpacerBlockRenderer";
|
|
25
|
-
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
26
|
-
import ProjectGridBlockRenderer from "./ProjectGridBlockRenderer";
|
|
27
|
-
import ProjectCarouselBlockRenderer from "./ProjectCarouselBlockRenderer";
|
|
20
|
+
import { getTextBlockStyles } from "./TextBlockRenderer";
|
|
21
|
+
import { getBlockRegistration } from "../../lib/builder/registry";
|
|
28
22
|
|
|
29
23
|
// ── BLK-003: Error Boundary for block renderers ──
|
|
30
24
|
// Prevents a single broken block from crashing the entire page.
|
|
@@ -289,44 +283,25 @@ export default function BlockRenderer({
|
|
|
289
283
|
|
|
290
284
|
let content: React.ReactNode;
|
|
291
285
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
case "projectGridBlock":
|
|
312
|
-
content = <ProjectGridBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectGridBlock} />;
|
|
313
|
-
break;
|
|
314
|
-
case "projectCarouselBlock":
|
|
315
|
-
content = <ProjectCarouselBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectCarouselBlock} />;
|
|
316
|
-
break;
|
|
317
|
-
default: {
|
|
318
|
-
const unknownBlock = resolved as ContentBlock;
|
|
319
|
-
if (process.env.NODE_ENV === "development") {
|
|
320
|
-
content = (
|
|
321
|
-
<div className="border border-dashed border-brand-secondary/50 p-4 font-mono text-xs text-brand-secondary">
|
|
322
|
-
Unknown block type: {unknownBlock._type}
|
|
323
|
-
</div>
|
|
324
|
-
);
|
|
325
|
-
} else {
|
|
326
|
-
// BLK-004: Log unknown block types in production for debugging
|
|
327
|
-
console.warn(`[BlockRenderer] Unknown block type "${unknownBlock._type}" (key: ${unknownBlock._key}) — skipped`);
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
286
|
+
// Registry-driven dispatch. Same result as the former switch-case — the
|
|
287
|
+
// Session B consistency tests guarantee registry.renderer === the imported
|
|
288
|
+
// component that was previously inlined in each case branch.
|
|
289
|
+
const registration = getBlockRegistration(resolved._type);
|
|
290
|
+
if (registration) {
|
|
291
|
+
const Renderer = registration.renderer as React.ComponentType<{ block: ContentBlock }>;
|
|
292
|
+
content = <Renderer block={resolved} />;
|
|
293
|
+
} else {
|
|
294
|
+
const unknownBlock = resolved as ContentBlock;
|
|
295
|
+
if (process.env.NODE_ENV === "development") {
|
|
296
|
+
content = (
|
|
297
|
+
<div className="border border-dashed border-brand-secondary/50 p-4 font-mono text-xs text-brand-secondary">
|
|
298
|
+
Unknown block type: {unknownBlock._type}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
// BLK-004: Log unknown block types in production for debugging
|
|
303
|
+
console.warn(`[BlockRenderer] Unknown block type "${unknownBlock._type}" (key: ${unknownBlock._key}) — skipped`);
|
|
304
|
+
return null;
|
|
330
305
|
}
|
|
331
306
|
}
|
|
332
307
|
|
|
@@ -14,26 +14,8 @@ import { memo } from "react";
|
|
|
14
14
|
import { resolveBlock } from "../../lib/builder/responsive";
|
|
15
15
|
import { getBlockLayoutStyles, hasBlockLayout } from "../../lib/builder/layout-styles";
|
|
16
16
|
import type { DeviceViewport } from "../../lib/builder/types";
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
TextBlock,
|
|
20
|
-
ImageBlock,
|
|
21
|
-
ImageGridBlock,
|
|
22
|
-
VideoBlock,
|
|
23
|
-
SpacerBlock,
|
|
24
|
-
ButtonBlock,
|
|
25
|
-
ProjectGridBlock,
|
|
26
|
-
ProjectCarouselBlock,
|
|
27
|
-
} from "../../lib/sanity/types";
|
|
28
|
-
|
|
29
|
-
import { LiveTextEditor } from "./live-preview";
|
|
30
|
-
import { LiveImagePreview } from "./live-preview";
|
|
31
|
-
import { LiveImageGridPreview } from "./live-preview";
|
|
32
|
-
import { LiveVideoPreview } from "./live-preview";
|
|
33
|
-
import { LiveSpacerPreview } from "./live-preview";
|
|
34
|
-
import { LiveButtonPreview } from "./live-preview";
|
|
35
|
-
import { LiveProjectGridPreview } from "./live-preview";
|
|
36
|
-
import { LiveProjectCarouselPreview } from "./live-preview";
|
|
17
|
+
import type { ContentBlock } from "../../lib/sanity/types";
|
|
18
|
+
import { getBlockRegistration } from "../../lib/builder/registry";
|
|
37
19
|
import { LivePlaceholder } from "./live-preview";
|
|
38
20
|
|
|
39
21
|
// ============================================
|
|
@@ -54,33 +36,22 @@ function BlockLivePreviewInner({ block, viewport = "desktop", editable = false }
|
|
|
54
36
|
|
|
55
37
|
let content: React.ReactNode;
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
case "buttonBlock":
|
|
74
|
-
content = <LiveButtonPreview block={resolved as ButtonBlock} />;
|
|
75
|
-
break;
|
|
76
|
-
case "projectGridBlock":
|
|
77
|
-
content = <LiveProjectGridPreview block={resolved as ProjectGridBlock} viewport={viewport} />;
|
|
78
|
-
break;
|
|
79
|
-
case "projectCarouselBlock":
|
|
80
|
-
content = <LiveProjectCarouselPreview block={resolved as ProjectCarouselBlock} viewport={viewport} />;
|
|
81
|
-
break;
|
|
82
|
-
default:
|
|
83
|
-
content = <LivePlaceholder type={(resolved as ContentBlock)._type} />;
|
|
39
|
+
// Registry-driven dispatch. The registration's `livePreview` component
|
|
40
|
+
// is guaranteed (by Session B consistency tests) to be the same as the
|
|
41
|
+
// component previously imported and inlined in each switch case. Each
|
|
42
|
+
// live preview accepts an overlapping props subset — we pass the full
|
|
43
|
+
// set (block, viewport, editable) and individual previews ignore what
|
|
44
|
+
// they don't need.
|
|
45
|
+
const registration = getBlockRegistration(resolved._type);
|
|
46
|
+
if (registration) {
|
|
47
|
+
const LivePreview = registration.livePreview as React.ComponentType<{
|
|
48
|
+
block: ContentBlock;
|
|
49
|
+
viewport?: DeviceViewport;
|
|
50
|
+
editable?: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
content = <LivePreview block={resolved} viewport={viewport} editable={editable} />;
|
|
53
|
+
} else {
|
|
54
|
+
content = <LivePlaceholder type={(resolved as ContentBlock)._type} />;
|
|
84
55
|
}
|
|
85
56
|
|
|
86
57
|
// Wrap in layout div if block has layout properties set (spacing, background, border, etc.)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
6
|
-
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
7
|
-
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
6
|
+
import type { PageSectionV2, CoverSection, SectionColumn } from "../../lib/sanity/types";
|
|
7
|
+
import { isPageSectionV2, isCoverSection } from "../../lib/sanity/types";
|
|
8
8
|
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
9
9
|
|
|
10
10
|
interface ColumnDragOverlayProps {
|
|
@@ -20,14 +20,29 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
|
20
20
|
}: ColumnDragOverlayProps) {
|
|
21
21
|
const rows = useBuilderStore((s) => s.rows);
|
|
22
22
|
const item = rows.find((r) => r._key === sectionKey);
|
|
23
|
-
if (!item
|
|
23
|
+
if (!item) return null;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Accept both PageSectionV2 and CoverSection — both expose `columns:
|
|
26
|
+
// SectionColumn[]` and `settings.grid_columns`. Other section types
|
|
27
|
+
// (parallax, custom instance) are not column-draggable targets.
|
|
28
|
+
let columns: SectionColumn[] | undefined;
|
|
29
|
+
let gridColumns = 12;
|
|
30
|
+
if (isPageSectionV2(item)) {
|
|
31
|
+
const v2 = item as PageSectionV2;
|
|
32
|
+
columns = v2.columns;
|
|
33
|
+
gridColumns = v2.settings?.grid_columns || 12;
|
|
34
|
+
} else if (isCoverSection(item)) {
|
|
35
|
+
const cover = item as CoverSection;
|
|
36
|
+
columns = cover.columns;
|
|
37
|
+
gridColumns = cover.settings?.grid_columns || 12;
|
|
38
|
+
} else {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const col = columns?.find((c) => c._key === columnKey);
|
|
27
43
|
if (!col) return null;
|
|
28
44
|
|
|
29
45
|
const blockCount = (col.blocks || []).length;
|
|
30
|
-
const gridColumns = v2Section.settings.grid_columns || 12;
|
|
31
46
|
|
|
32
47
|
const overlay = (
|
|
33
48
|
<div
|
|
@@ -2,12 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
4
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
-
import type { PageSectionV2 } from "../../../lib/sanity/types";
|
|
6
|
-
import { isPageSectionV2 } from "../../../lib/sanity/types";
|
|
5
|
+
import type { PageSectionV2, CoverSection, ContentItem } from "../../../lib/sanity/types";
|
|
6
|
+
import { isPageSectionV2, isCoverSection } from "../../../lib/sanity/types";
|
|
7
7
|
import { getEffectiveColumnsV2, buildColumnV2Overrides } from "../settings-panel/responsive-helpers";
|
|
8
8
|
import { moveColumn as cascadeMoveColumn } from "../../../lib/builder/cascade";
|
|
9
9
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* View a Cover section as a PageSectionV2 for DnD purposes.
|
|
13
|
+
*
|
|
14
|
+
* Cover and V2 share the same column shape, grid_columns, and
|
|
15
|
+
* `responsive[vp].columns` shape — the fields read/written by column
|
|
16
|
+
* DnD. This shim lets us reuse getEffectiveColumnsV2/buildColumnV2Overrides
|
|
17
|
+
* without duplicating logic. Only `columns`, `settings.grid_columns`, and
|
|
18
|
+
* `responsive[vp].columns` are exposed; Cover-specific fields
|
|
19
|
+
* (`cover_rows`, Cover-specific `settings`) are hidden from the V2 helpers.
|
|
20
|
+
*
|
|
21
|
+
* The write-back path (`updateSectionV2Responsive` → `updateSectionAtPath`)
|
|
22
|
+
* merges column overrides back into the cover, preserving Cover-specific
|
|
23
|
+
* fields. See `store-helpers.ts :: updateSectionAtPath` cover branch.
|
|
24
|
+
*/
|
|
25
|
+
function coverAsV2(cover: CoverSection): PageSectionV2 {
|
|
26
|
+
return {
|
|
27
|
+
_type: "pageSectionV2",
|
|
28
|
+
_key: cover._key,
|
|
29
|
+
section_type: "empty-v2",
|
|
30
|
+
columns: cover.columns,
|
|
31
|
+
settings: {
|
|
32
|
+
preset: "custom",
|
|
33
|
+
grid_columns: cover.settings.grid_columns,
|
|
34
|
+
col_gap: cover.settings.col_gap,
|
|
35
|
+
row_gap: cover.settings.row_gap,
|
|
36
|
+
},
|
|
37
|
+
responsive:
|
|
38
|
+
cover.responsive?.tablet?.columns || cover.responsive?.phone?.columns
|
|
39
|
+
? {
|
|
40
|
+
...(cover.responsive.tablet?.columns
|
|
41
|
+
? { tablet: { columns: cover.responsive.tablet.columns } }
|
|
42
|
+
: {}),
|
|
43
|
+
...(cover.responsive.phone?.columns
|
|
44
|
+
? { phone: { columns: cover.responsive.phone.columns } }
|
|
45
|
+
: {}),
|
|
46
|
+
}
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the section by key and return a V2-shaped view of it (PageSectionV2
|
|
53
|
+
* directly, or Cover adapted via `coverAsV2`). Returns null if the section
|
|
54
|
+
* doesn't exist or is an unsupported type (parallax, custom instance).
|
|
55
|
+
*/
|
|
56
|
+
function findColumnarSection(
|
|
57
|
+
rows: ContentItem[],
|
|
58
|
+
sectionKey: string,
|
|
59
|
+
): PageSectionV2 | null {
|
|
60
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
61
|
+
if (!section) return null;
|
|
62
|
+
if (isPageSectionV2(section)) return section as PageSectionV2;
|
|
63
|
+
if (isCoverSection(section)) return coverAsV2(section as CoverSection);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
11
67
|
// ============================================
|
|
12
68
|
// Types
|
|
13
69
|
// ============================================
|
|
@@ -57,9 +113,8 @@ function executeResponsiveSwap(
|
|
|
57
113
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
58
114
|
): void {
|
|
59
115
|
const rows = useBuilderStore.getState().rows;
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
const v2Section = section as PageSectionV2;
|
|
116
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
117
|
+
if (!v2Section) return;
|
|
63
118
|
|
|
64
119
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
65
120
|
const draggedCol = effectiveCols.find((c) => c._key === draggedKey);
|
|
@@ -109,9 +164,8 @@ function executeResponsiveGapMove(
|
|
|
109
164
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
110
165
|
): void {
|
|
111
166
|
const rows = useBuilderStore.getState().rows;
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
const v2Section = section as PageSectionV2;
|
|
167
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
168
|
+
if (!v2Section) return;
|
|
115
169
|
|
|
116
170
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
117
171
|
const columnOverrides = effectiveCols.map((c) => {
|
|
@@ -147,9 +201,8 @@ function executeResponsiveInsert(
|
|
|
147
201
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
148
202
|
): void {
|
|
149
203
|
const rows = useBuilderStore.getState().rows;
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
const v2Section = section as PageSectionV2;
|
|
204
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
205
|
+
if (!v2Section) return;
|
|
153
206
|
|
|
154
207
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
155
208
|
const cascadeResult = cascadeMoveColumn(effectiveCols, columnKey, targetRow, targetCol, v2Section.settings.grid_columns);
|
|
@@ -4,19 +4,13 @@
|
|
|
4
4
|
* BlockSettings — Delegates to type-specific block editors.
|
|
5
5
|
*
|
|
6
6
|
* Session 64: Extracted from SettingsPanel.tsx.
|
|
7
|
+
* Session 181 (C): Switched from explicit switch-case dispatch to a
|
|
8
|
+
* registry lookup — the editor component for each block type is
|
|
9
|
+
* registered in `lib/builder/block-registrations.ts`.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import type { ContentBlock } from "../../../lib/sanity/types";
|
|
10
|
-
import {
|
|
11
|
-
TextBlockEditor,
|
|
12
|
-
ImageBlockEditor,
|
|
13
|
-
ImageGridBlockEditor,
|
|
14
|
-
VideoBlockEditor,
|
|
15
|
-
SpacerBlockEditor,
|
|
16
|
-
ButtonBlockEditor,
|
|
17
|
-
ProjectGridEditor,
|
|
18
|
-
ProjectCarouselBlockEditor,
|
|
19
|
-
} from "../editors";
|
|
13
|
+
import { getBlockRegistration } from "../../../lib/builder/registry";
|
|
20
14
|
|
|
21
15
|
export default function BlockSettings({
|
|
22
16
|
block,
|
|
@@ -27,68 +21,22 @@ export default function BlockSettings({
|
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
// ============================================
|
|
30
|
-
// Block Type Editor Router
|
|
24
|
+
// Block Type Editor Router (registry-driven)
|
|
31
25
|
// ============================================
|
|
32
26
|
|
|
33
27
|
function BlockTypeEditor({ block }: { block: ContentBlock }) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
block={block as import("../../../lib/sanity/types").TextBlock}
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
case "imageBlock":
|
|
42
|
-
return (
|
|
43
|
-
<ImageBlockEditor
|
|
44
|
-
block={block as import("../../../lib/sanity/types").ImageBlock}
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
case "imageGridBlock":
|
|
48
|
-
return (
|
|
49
|
-
<ImageGridBlockEditor
|
|
50
|
-
block={block as import("../../../lib/sanity/types").ImageGridBlock}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
case "videoBlock":
|
|
54
|
-
return (
|
|
55
|
-
<VideoBlockEditor
|
|
56
|
-
block={block as import("../../../lib/sanity/types").VideoBlock}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
case "spacerBlock":
|
|
60
|
-
return (
|
|
61
|
-
<SpacerBlockEditor
|
|
62
|
-
block={block as import("../../../lib/sanity/types").SpacerBlock}
|
|
63
|
-
/>
|
|
64
|
-
);
|
|
65
|
-
case "buttonBlock":
|
|
66
|
-
return (
|
|
67
|
-
<ButtonBlockEditor
|
|
68
|
-
block={block as import("../../../lib/sanity/types").ButtonBlock}
|
|
69
|
-
/>
|
|
70
|
-
);
|
|
71
|
-
case "projectGridBlock":
|
|
72
|
-
return (
|
|
73
|
-
<ProjectGridEditor
|
|
74
|
-
block={block as import("../../../lib/sanity/types").ProjectGridBlock}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
case "projectCarouselBlock":
|
|
78
|
-
return (
|
|
79
|
-
<ProjectCarouselBlockEditor
|
|
80
|
-
block={block as import("../../../lib/sanity/types").ProjectCarouselBlock}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
default:
|
|
84
|
-
return (
|
|
85
|
-
<div className="p-4">
|
|
86
|
-
<div className="rounded-lg bg-[#f5f5f5] p-3">
|
|
87
|
-
<p className="text-xs text-neutral-400">
|
|
88
|
-
No editor available for this block type.
|
|
89
|
-
</p>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
28
|
+
const registration = getBlockRegistration(block._type);
|
|
29
|
+
if (registration) {
|
|
30
|
+
const Editor = registration.editor as React.ComponentType<{ block: ContentBlock }>;
|
|
31
|
+
return <Editor block={block} />;
|
|
93
32
|
}
|
|
33
|
+
return (
|
|
34
|
+
<div className="p-4">
|
|
35
|
+
<div className="rounded-lg bg-[#f5f5f5] p-3">
|
|
36
|
+
<p className="text-xs text-neutral-400">
|
|
37
|
+
No editor available for this block type.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
94
42
|
}
|
|
@@ -18,10 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { registerBlockType } from "./block-registry";
|
|
21
|
-
import { createDefaultBlock } from "./defaults";
|
|
22
21
|
|
|
23
22
|
import type {
|
|
24
|
-
ContentBlock,
|
|
25
23
|
TextBlock,
|
|
26
24
|
ImageBlock,
|
|
27
25
|
ImageGridBlock,
|
|
@@ -107,24 +105,15 @@ import {
|
|
|
107
105
|
} from "../../components/builder/blockStyles";
|
|
108
106
|
|
|
109
107
|
// ────────────────────────────────────────────────────────────────────
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
type as Parameters<typeof createDefaultBlock>[0],
|
|
120
|
-
);
|
|
121
|
-
return { ...block, _key: key } as T;
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ────────────────────────────────────────────────────────────────────
|
|
126
|
-
// Registrations (order matches the existing BLOCK_TYPE_REGISTRY +
|
|
127
|
-
// section blocks appended at the end, so iteration order is stable).
|
|
108
|
+
// Registrations.
|
|
109
|
+
//
|
|
110
|
+
// Each `defaultFactory` is inlined here (rather than delegating to
|
|
111
|
+
// `createDefaultBlock` in `./defaults.ts`) to avoid a circular dependency:
|
|
112
|
+
// defaults.ts -> block-registry.getBlockRegistration
|
|
113
|
+
// defaults.ts -> side-effect: ./block-registrations
|
|
114
|
+
// block-registrations.ts -> (no longer imports ./defaults)
|
|
115
|
+
// Order matches the existing BLOCK_TYPE_REGISTRY + section blocks appended
|
|
116
|
+
// at the end, so iteration order is stable.
|
|
128
117
|
// ────────────────────────────────────────────────────────────────────
|
|
129
118
|
|
|
130
119
|
// ── Content blocks ──
|
|
@@ -136,7 +125,12 @@ registerBlockType<TextBlock>({
|
|
|
136
125
|
category: "content",
|
|
137
126
|
iconGlyph: "T",
|
|
138
127
|
schema: textBlock,
|
|
139
|
-
defaultFactory:
|
|
128
|
+
defaultFactory: (key) => ({
|
|
129
|
+
_type: "textBlock",
|
|
130
|
+
_key: key,
|
|
131
|
+
text: [],
|
|
132
|
+
style: { fontSize: 14, alignment: "left", fontWeight: "400" },
|
|
133
|
+
}),
|
|
140
134
|
renderer: TextBlockRenderer as React.ComponentType<{ block: TextBlock }>,
|
|
141
135
|
livePreview: LiveTextEditor as unknown as React.ComponentType<{ block: TextBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
142
136
|
editor: TextBlockEditor as React.ComponentType<{ block: TextBlock }>,
|
|
@@ -153,7 +147,17 @@ registerBlockType<ImageBlock>({
|
|
|
153
147
|
category: "content",
|
|
154
148
|
iconGlyph: "🖼",
|
|
155
149
|
schema: imageBlock,
|
|
156
|
-
defaultFactory:
|
|
150
|
+
defaultFactory: (key) => ({
|
|
151
|
+
_type: "imageBlock",
|
|
152
|
+
_key: key,
|
|
153
|
+
asset_path: "",
|
|
154
|
+
alt: "",
|
|
155
|
+
width: "full",
|
|
156
|
+
aspect_ratio: "auto",
|
|
157
|
+
lazy: true,
|
|
158
|
+
shadow: false,
|
|
159
|
+
border_radius: "",
|
|
160
|
+
}),
|
|
157
161
|
renderer: ImageBlockRenderer as React.ComponentType<{ block: ImageBlock }>,
|
|
158
162
|
livePreview: LiveImagePreview as unknown as React.ComponentType<{ block: ImageBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
159
163
|
editor: ImageBlockEditor as React.ComponentType<{ block: ImageBlock }>,
|
|
@@ -170,7 +174,18 @@ registerBlockType<ImageGridBlock>({
|
|
|
170
174
|
category: "content",
|
|
171
175
|
iconGlyph: "⊞",
|
|
172
176
|
schema: imageGridBlock,
|
|
173
|
-
defaultFactory:
|
|
177
|
+
defaultFactory: (key) => ({
|
|
178
|
+
_type: "imageGridBlock",
|
|
179
|
+
_key: key,
|
|
180
|
+
images: [],
|
|
181
|
+
h_gutter: 10,
|
|
182
|
+
v_gutter: 10,
|
|
183
|
+
images_per_row: 2,
|
|
184
|
+
random_grid: "disabled",
|
|
185
|
+
random_seed: 1,
|
|
186
|
+
lightbox: false,
|
|
187
|
+
object_fit: "cover",
|
|
188
|
+
}),
|
|
174
189
|
renderer: ImageGridBlockRenderer as React.ComponentType<{ block: ImageGridBlock }>,
|
|
175
190
|
livePreview: LiveImageGridPreview as unknown as React.ComponentType<{ block: ImageGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
176
191
|
editor: ImageGridBlockEditor as React.ComponentType<{ block: ImageGridBlock }>,
|
|
@@ -187,7 +202,17 @@ registerBlockType<VideoBlock>({
|
|
|
187
202
|
category: "content",
|
|
188
203
|
iconGlyph: "▶",
|
|
189
204
|
schema: videoBlock,
|
|
190
|
-
defaultFactory:
|
|
205
|
+
defaultFactory: (key) => ({
|
|
206
|
+
_type: "videoBlock",
|
|
207
|
+
_key: key,
|
|
208
|
+
video_type: "vimeo",
|
|
209
|
+
url_or_path: "",
|
|
210
|
+
autoplay: false,
|
|
211
|
+
loop: false,
|
|
212
|
+
muted: true,
|
|
213
|
+
controls: true,
|
|
214
|
+
aspect_ratio: "16:9",
|
|
215
|
+
}),
|
|
191
216
|
renderer: VideoBlockRenderer as React.ComponentType<{ block: VideoBlock }>,
|
|
192
217
|
livePreview: LiveVideoPreview as unknown as React.ComponentType<{ block: VideoBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
193
218
|
editor: VideoBlockEditor as React.ComponentType<{ block: VideoBlock }>,
|
|
@@ -204,7 +229,11 @@ registerBlockType<SpacerBlock>({
|
|
|
204
229
|
category: "content",
|
|
205
230
|
iconGlyph: "↕",
|
|
206
231
|
schema: spacerBlock,
|
|
207
|
-
defaultFactory:
|
|
232
|
+
defaultFactory: (key) => ({
|
|
233
|
+
_type: "spacerBlock",
|
|
234
|
+
_key: key,
|
|
235
|
+
height: "medium",
|
|
236
|
+
}),
|
|
208
237
|
renderer: SpacerBlockRenderer as React.ComponentType<{ block: SpacerBlock }>,
|
|
209
238
|
livePreview: LiveSpacerPreview as unknown as React.ComponentType<{ block: SpacerBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
210
239
|
editor: SpacerBlockEditor as React.ComponentType<{ block: SpacerBlock }>,
|
|
@@ -221,7 +250,14 @@ registerBlockType<ButtonBlock>({
|
|
|
221
250
|
category: "content",
|
|
222
251
|
iconGlyph: "▣",
|
|
223
252
|
schema: buttonBlock,
|
|
224
|
-
defaultFactory:
|
|
253
|
+
defaultFactory: (key) => ({
|
|
254
|
+
_type: "buttonBlock",
|
|
255
|
+
_key: key,
|
|
256
|
+
text: "Button",
|
|
257
|
+
url: "#",
|
|
258
|
+
style: "primary",
|
|
259
|
+
size: "medium",
|
|
260
|
+
}),
|
|
225
261
|
renderer: ButtonBlockRenderer as React.ComponentType<{ block: ButtonBlock }>,
|
|
226
262
|
livePreview: LiveButtonPreview as unknown as React.ComponentType<{ block: ButtonBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
227
263
|
editor: ButtonBlockEditor as React.ComponentType<{ block: ButtonBlock }>,
|
|
@@ -240,7 +276,19 @@ registerBlockType<ProjectGridBlock>({
|
|
|
240
276
|
category: "section",
|
|
241
277
|
iconGlyph: "⬡",
|
|
242
278
|
schema: projectGridBlock,
|
|
243
|
-
defaultFactory:
|
|
279
|
+
defaultFactory: (key) => ({
|
|
280
|
+
_type: "projectGridBlock",
|
|
281
|
+
_key: key,
|
|
282
|
+
columns: 3,
|
|
283
|
+
aspect_ratios: ["16/9"],
|
|
284
|
+
gap_v: 16,
|
|
285
|
+
gap_h: 16,
|
|
286
|
+
hover_effect: "scale",
|
|
287
|
+
show_subtitle: true,
|
|
288
|
+
border_radius: 0,
|
|
289
|
+
video_mode: "off",
|
|
290
|
+
projects: [],
|
|
291
|
+
}),
|
|
244
292
|
renderer: ProjectGridBlockRenderer as React.ComponentType<{ block: ProjectGridBlock }>,
|
|
245
293
|
livePreview: LiveProjectGridPreview as unknown as React.ComponentType<{ block: ProjectGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
246
294
|
editor: ProjectGridEditor as React.ComponentType<{ block: ProjectGridBlock }>,
|
|
@@ -257,7 +305,26 @@ registerBlockType<ProjectCarouselBlock>({
|
|
|
257
305
|
category: "section",
|
|
258
306
|
iconGlyph: "▸",
|
|
259
307
|
schema: projectCarouselBlock,
|
|
260
|
-
defaultFactory:
|
|
308
|
+
defaultFactory: (key) => ({
|
|
309
|
+
_type: "projectCarouselBlock",
|
|
310
|
+
_key: key,
|
|
311
|
+
source_mode: "auto_latest",
|
|
312
|
+
max_projects: 8,
|
|
313
|
+
exclude_current: true,
|
|
314
|
+
cards_per_view_desktop: 3.5,
|
|
315
|
+
cards_per_view_tablet: 2.2,
|
|
316
|
+
cards_per_view_phone: 1.2,
|
|
317
|
+
gap: 16,
|
|
318
|
+
aspect_ratio: "4/3",
|
|
319
|
+
show_title: true,
|
|
320
|
+
show_subtitle: false,
|
|
321
|
+
border_radius: 0,
|
|
322
|
+
hover_effect: "scale",
|
|
323
|
+
video_mode: "off",
|
|
324
|
+
show_arrows: true,
|
|
325
|
+
show_dots: false,
|
|
326
|
+
snap_scroll: true,
|
|
327
|
+
}),
|
|
261
328
|
renderer: ProjectCarouselBlockRenderer as React.ComponentType<{ block: ProjectCarouselBlock }>,
|
|
262
329
|
livePreview: LiveProjectCarouselPreview as unknown as React.ComponentType<{ block: ProjectCarouselBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
263
330
|
editor: ProjectCarouselBlockEditor as React.ComponentType<{ block: ProjectCarouselBlock }>,
|