@morphika/andami 0.3.1 → 0.4.1
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/app/admin/pages/[slug]/page.tsx +2 -2
- package/components/blocks/BlockRenderer.tsx +21 -42
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
- package/components/builder/BlockLivePreview.tsx +18 -42
- package/components/builder/ReadOnlyFrame.tsx +4 -23
- package/components/builder/SectionCardIcons.tsx +54 -0
- package/components/builder/SectionTypePicker.tsx +1 -1
- package/components/builder/blockStyles.tsx +6 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
- package/components/builder/editors/index.ts +1 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
- package/components/builder/live-preview/index.ts +1 -0
- package/components/builder/settings-panel/BlockSettings.tsx +18 -63
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +335 -0
- package/lib/builder/block-registry.ts +195 -0
- package/lib/builder/defaults.ts +22 -81
- package/lib/builder/index.ts +16 -0
- package/lib/builder/registry.ts +44 -0
- package/lib/builder/store-sections.ts +1 -1
- package/lib/builder/types.ts +8 -3
- package/lib/sanity/types.ts +50 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/index.ts +2 -1
- package/sanity/schemas/blocks/projectCarouselBlock.ts +218 -0
- package/sanity/schemas/index.ts +4 -1
- package/sanity/schemas/pageSectionV2.ts +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LiveProjectCarouselPreview — Builder canvas preview for projectCarouselBlock.
|
|
5
|
+
*
|
|
6
|
+
* Renders N mockup cards (respecting `max_projects`, aspect_ratio, gap,
|
|
7
|
+
* cards-per-view per viewport, border_radius) so the designer can evaluate
|
|
8
|
+
* the layout inside the builder without hitting the projects API. Real
|
|
9
|
+
* thumbnails show up on the public site.
|
|
10
|
+
*
|
|
11
|
+
* Mock card visual matches the empty-state placeholder of the Image / Video
|
|
12
|
+
* blocks: soft grey background + centered landscape (or play) icon. Keeps
|
|
13
|
+
* the preview readable at any zoom level and communicates that the real
|
|
14
|
+
* content is image/video thumbnails without faking actual data.
|
|
15
|
+
*
|
|
16
|
+
* Intentionally independent from LiveProjectGridPreview — same design
|
|
17
|
+
* language, no shared code.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
21
|
+
import type { ProjectCarouselBlock } from "../../../lib/sanity/types";
|
|
22
|
+
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
23
|
+
|
|
24
|
+
// ─── Helpers (duplicated from public renderer on purpose — per user request
|
|
25
|
+
// the two carousels are not linked so changes on one side can't cascade). ──
|
|
26
|
+
|
|
27
|
+
function resolveCardsPerView(
|
|
28
|
+
block: Pick<ProjectCarouselBlock, "cards_per_view_desktop" | "cards_per_view_tablet" | "cards_per_view_phone">,
|
|
29
|
+
viewport: DeviceViewport,
|
|
30
|
+
): number {
|
|
31
|
+
if (viewport === "phone") return block.cards_per_view_phone ?? 1.2;
|
|
32
|
+
if (viewport === "tablet") return block.cards_per_view_tablet ?? 2.2;
|
|
33
|
+
return block.cards_per_view_desktop ?? 3.5;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function computeCardWidth(
|
|
37
|
+
containerWidth: number,
|
|
38
|
+
cardsPerView: number,
|
|
39
|
+
gap: number,
|
|
40
|
+
): number {
|
|
41
|
+
if (cardsPerView <= 0 || containerWidth <= 0) return 0;
|
|
42
|
+
return Math.max(0, (containerWidth - (cardsPerView - 1) * gap) / cardsPerView);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Mock card glyphs ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function LandscapeGlyph() {
|
|
48
|
+
return (
|
|
49
|
+
<svg width="48" height="48" viewBox="0 0 56 56" fill="none" aria-hidden="true">
|
|
50
|
+
<rect x="6" y="10" width="44" height="36" rx="3" stroke="#b0b5bd" strokeWidth="1.5" fill="#FFFFFF" />
|
|
51
|
+
<circle cx="18" cy="21" r="3" fill="#b0b5bd" />
|
|
52
|
+
<path d="M12 42 L22 28 L28 34 L38 22 L46 42 Z" fill="#b0b5bd" />
|
|
53
|
+
</svg>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function PlayGlyph() {
|
|
58
|
+
return (
|
|
59
|
+
<svg width="48" height="48" viewBox="0 0 56 56" fill="none" aria-hidden="true">
|
|
60
|
+
<circle cx="28" cy="28" r="22" fill="#FFFFFF" stroke="#b0b5bd" strokeWidth="1.5" />
|
|
61
|
+
<path d="M24 20 L37 28 L24 36 Z" fill="#b0b5bd" />
|
|
62
|
+
</svg>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Main component ──────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export default function LiveProjectCarouselPreview({
|
|
69
|
+
block,
|
|
70
|
+
viewport: frameViewport = "desktop",
|
|
71
|
+
}: {
|
|
72
|
+
block: ProjectCarouselBlock;
|
|
73
|
+
viewport?: DeviceViewport;
|
|
74
|
+
}) {
|
|
75
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
76
|
+
const roRef = useRef<ResizeObserver | null>(null);
|
|
77
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
78
|
+
|
|
79
|
+
// Config
|
|
80
|
+
const maxProjects = block.max_projects ?? 8;
|
|
81
|
+
const gap = block.gap ?? 16;
|
|
82
|
+
const aspectRatio = block.aspect_ratio ?? "4/3";
|
|
83
|
+
const showTitle = block.show_title !== false;
|
|
84
|
+
const showSubtitle = block.show_subtitle === true;
|
|
85
|
+
const borderRadius = block.border_radius ?? 0;
|
|
86
|
+
const snapScroll = block.snap_scroll !== false;
|
|
87
|
+
const videoMode = block.video_mode ?? "off";
|
|
88
|
+
const showDots = block.show_dots === true;
|
|
89
|
+
|
|
90
|
+
const cardsPerView = resolveCardsPerView(block, frameViewport);
|
|
91
|
+
const cardWidth = useMemo(
|
|
92
|
+
() => computeCardWidth(containerWidth, cardsPerView, gap),
|
|
93
|
+
[containerWidth, cardsPerView, gap],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// ResizeObserver
|
|
97
|
+
const containerCallbackRef = useCallback((node: HTMLDivElement | null) => {
|
|
98
|
+
if (roRef.current) {
|
|
99
|
+
roRef.current.disconnect();
|
|
100
|
+
roRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
containerRef.current = node;
|
|
103
|
+
if (!node) return;
|
|
104
|
+
|
|
105
|
+
const ro = new ResizeObserver((entries) => {
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const w = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
108
|
+
if (w > 0) setContainerWidth(w);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
roRef.current = ro;
|
|
112
|
+
ro.observe(node);
|
|
113
|
+
|
|
114
|
+
const measure = () => {
|
|
115
|
+
const w = node.clientWidth;
|
|
116
|
+
if (w > 0) setContainerWidth(w);
|
|
117
|
+
};
|
|
118
|
+
measure();
|
|
119
|
+
requestAnimationFrame(measure);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
useEffect(
|
|
123
|
+
() => () => {
|
|
124
|
+
if (roRef.current) {
|
|
125
|
+
roRef.current.disconnect();
|
|
126
|
+
roRef.current = null;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Build the mock project array — just indexes, each card shows its number.
|
|
133
|
+
const mockCards = useMemo(
|
|
134
|
+
() => Array.from({ length: Math.max(2, Math.min(20, maxProjects)) }, (_, i) => i),
|
|
135
|
+
[maxProjects],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const Glyph = videoMode !== "off" ? PlayGlyph : LandscapeGlyph;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div ref={containerCallbackRef} className="relative w-full">
|
|
142
|
+
{/* Scroll track — same mechanics as public renderer so layout is faithful */}
|
|
143
|
+
<div
|
|
144
|
+
className="flex overflow-x-auto"
|
|
145
|
+
style={{
|
|
146
|
+
gap: `${gap}px`,
|
|
147
|
+
scrollSnapType: snapScroll ? "x mandatory" : undefined,
|
|
148
|
+
scrollBehavior: "smooth",
|
|
149
|
+
WebkitOverflowScrolling: "touch",
|
|
150
|
+
// Hide native scrollbar
|
|
151
|
+
scrollbarWidth: "none",
|
|
152
|
+
msOverflowStyle: "none",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{mockCards.map((i) => (
|
|
156
|
+
<div
|
|
157
|
+
key={i}
|
|
158
|
+
className="flex flex-col"
|
|
159
|
+
style={{
|
|
160
|
+
flex: `0 0 ${cardWidth}px`,
|
|
161
|
+
scrollSnapAlign: snapScroll ? "start" : "none",
|
|
162
|
+
minWidth: 0,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{/* Thumbnail area — mock */}
|
|
166
|
+
<div
|
|
167
|
+
className="relative w-full flex items-center justify-center"
|
|
168
|
+
style={{
|
|
169
|
+
aspectRatio: aspectRatio.replace("/", " / "),
|
|
170
|
+
background: "#f4f4f4",
|
|
171
|
+
borderRadius: borderRadius ? `${borderRadius}px` : undefined,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<Glyph />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Placeholder title + subtitle */}
|
|
178
|
+
{(showTitle || showSubtitle) && (
|
|
179
|
+
<div className="mt-3 px-0.5">
|
|
180
|
+
{showTitle && (
|
|
181
|
+
<div
|
|
182
|
+
className="h-[10px] rounded-sm"
|
|
183
|
+
style={{ background: "#e0e0e0", width: "60%" }}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
{showSubtitle && (
|
|
187
|
+
<div
|
|
188
|
+
className="h-[8px] rounded-sm mt-1.5"
|
|
189
|
+
style={{ background: "#ededed", width: "40%" }}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Dots preview — decorative only inside the builder */}
|
|
199
|
+
{showDots && mockCards.length > 1 && (
|
|
200
|
+
<div className="flex justify-center gap-1.5 mt-4">
|
|
201
|
+
{mockCards.slice(0, Math.min(6, mockCards.length)).map((i) => (
|
|
202
|
+
<span
|
|
203
|
+
key={i}
|
|
204
|
+
style={{
|
|
205
|
+
width: i === 0 ? 24 : 6,
|
|
206
|
+
height: 6,
|
|
207
|
+
borderRadius: 999,
|
|
208
|
+
background: i === 0 ? "#2b2f38" : "#c9c9c9",
|
|
209
|
+
display: "inline-block",
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Tiny info strip so the designer knows this is a preview */}
|
|
217
|
+
<div className="absolute top-1 right-1 pointer-events-none text-[9px] font-medium px-1.5 py-0.5 rounded bg-white/80 border border-black/10 text-neutral-500 shadow-sm">
|
|
218
|
+
Preview · {mockCards.length} mock cards
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Chrome-style inline CSS to hide the webkit scrollbar */}
|
|
222
|
+
<style>{`
|
|
223
|
+
.flex::-webkit-scrollbar { display: none; }
|
|
224
|
+
`}</style>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -8,4 +8,5 @@ export { default as LiveVideoPreview } from "./LiveVideoPreview";
|
|
|
8
8
|
export { default as LiveSpacerPreview } from "./LiveSpacerPreview";
|
|
9
9
|
export { default as LiveButtonPreview } from "./LiveButtonPreview";
|
|
10
10
|
export { default as LiveProjectGridPreview } from "./LiveProjectGridPreview";
|
|
11
|
+
export { default as LiveProjectCarouselPreview } from "./LiveProjectCarouselPreview";
|
|
11
12
|
export { ThumbBadge, LivePlaceholder, useProjectThumbnails, ProjectGridCard } from "./shared";
|
|
@@ -4,18 +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
|
-
} from "../editors";
|
|
13
|
+
import { getBlockRegistration } from "../../../lib/builder/registry";
|
|
19
14
|
|
|
20
15
|
export default function BlockSettings({
|
|
21
16
|
block,
|
|
@@ -26,62 +21,22 @@ export default function BlockSettings({
|
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
// ============================================
|
|
29
|
-
// Block Type Editor Router
|
|
24
|
+
// Block Type Editor Router (registry-driven)
|
|
30
25
|
// ============================================
|
|
31
26
|
|
|
32
27
|
function BlockTypeEditor({ block }: { block: ContentBlock }) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
block={block as import("../../../lib/sanity/types").TextBlock}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
case "imageBlock":
|
|
41
|
-
return (
|
|
42
|
-
<ImageBlockEditor
|
|
43
|
-
block={block as import("../../../lib/sanity/types").ImageBlock}
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
46
|
-
case "imageGridBlock":
|
|
47
|
-
return (
|
|
48
|
-
<ImageGridBlockEditor
|
|
49
|
-
block={block as import("../../../lib/sanity/types").ImageGridBlock}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
case "videoBlock":
|
|
53
|
-
return (
|
|
54
|
-
<VideoBlockEditor
|
|
55
|
-
block={block as import("../../../lib/sanity/types").VideoBlock}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
case "spacerBlock":
|
|
59
|
-
return (
|
|
60
|
-
<SpacerBlockEditor
|
|
61
|
-
block={block as import("../../../lib/sanity/types").SpacerBlock}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
case "buttonBlock":
|
|
65
|
-
return (
|
|
66
|
-
<ButtonBlockEditor
|
|
67
|
-
block={block as import("../../../lib/sanity/types").ButtonBlock}
|
|
68
|
-
/>
|
|
69
|
-
);
|
|
70
|
-
case "projectGridBlock":
|
|
71
|
-
return (
|
|
72
|
-
<ProjectGridEditor
|
|
73
|
-
block={block as import("../../../lib/sanity/types").ProjectGridBlock}
|
|
74
|
-
/>
|
|
75
|
-
);
|
|
76
|
-
default:
|
|
77
|
-
return (
|
|
78
|
-
<div className="p-4">
|
|
79
|
-
<div className="rounded-lg bg-[#f5f5f5] p-3">
|
|
80
|
-
<p className="text-xs text-neutral-400">
|
|
81
|
-
No editor available for this block type.
|
|
82
|
-
</p>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
28
|
+
const registration = getBlockRegistration(block._type);
|
|
29
|
+
if (registration) {
|
|
30
|
+
const Editor = registration.editor as React.ComponentType<{ block: ContentBlock }>;
|
|
31
|
+
return <Editor block={block} />;
|
|
86
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
|
+
);
|
|
87
42
|
}
|
|
@@ -75,6 +75,7 @@ export const BLOCK_ENTER_PRESETS: Record<BlockType, readonly EnterPreset[]> = {
|
|
|
75
75
|
buttonBlock: ["fade", "slide-up", "scale"],
|
|
76
76
|
spacerBlock: [], // invisible — no animation
|
|
77
77
|
projectGridBlock: [], // uses card_entrance system
|
|
78
|
+
projectCarouselBlock: [], // uses card_entrance system
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
// ── Enter animation config ─────────────────────────────────────────
|
|
@@ -68,6 +68,7 @@ export const BLOCK_HOVER_PRESETS: Record<BlockType, readonly HoverPreset[]> = {
|
|
|
68
68
|
buttonBlock: ["scale-up", "lift", "border-glow"],
|
|
69
69
|
spacerBlock: [], // invisible
|
|
70
70
|
projectGridBlock: ["scale-up", "lift"], // per-card effect
|
|
71
|
+
projectCarouselBlock: [], // uses per-card hover_effect field directly
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
// ── Hover effect config ────────────────────────────────────────────
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Block registrations — fills the registry defined in `./block-registry.ts`
|
|
5
|
+
* with the 8 built-in block types shipped by the framework.
|
|
6
|
+
*
|
|
7
|
+
* Importing this module has a side effect: every `registerBlockType()`
|
|
8
|
+
* call below runs synchronously and populates the module-level registry.
|
|
9
|
+
* Most of the codebase currently does NOT import this — the existing
|
|
10
|
+
* dispatch tables in `components/blocks/BlockRenderer.tsx`,
|
|
11
|
+
* `components/builder/BlockLivePreview.tsx`, etc. are still the source
|
|
12
|
+
* of truth at runtime. This file is the parallel structure we'll migrate
|
|
13
|
+
* to in a later session, behind the existing public API so instances
|
|
14
|
+
* don't break.
|
|
15
|
+
*
|
|
16
|
+
* Session A (2026-04-19): all 8 blocks registered; nothing consumes
|
|
17
|
+
* the registry yet.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { registerBlockType } from "./block-registry";
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
TextBlock,
|
|
24
|
+
ImageBlock,
|
|
25
|
+
ImageGridBlock,
|
|
26
|
+
VideoBlock,
|
|
27
|
+
SpacerBlock,
|
|
28
|
+
ButtonBlock,
|
|
29
|
+
ProjectGridBlock,
|
|
30
|
+
ProjectCarouselBlock,
|
|
31
|
+
} from "../sanity/types";
|
|
32
|
+
|
|
33
|
+
// ── Sanity schemas ────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
textBlock,
|
|
37
|
+
imageBlock,
|
|
38
|
+
imageGridBlock,
|
|
39
|
+
videoBlock,
|
|
40
|
+
spacerBlock,
|
|
41
|
+
buttonBlock,
|
|
42
|
+
projectGridBlock,
|
|
43
|
+
projectCarouselBlock,
|
|
44
|
+
} from "../../sanity/schemas/blocks";
|
|
45
|
+
|
|
46
|
+
// ── Public renderers ──────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
import TextBlockRenderer from "../../components/blocks/TextBlockRenderer";
|
|
49
|
+
import ImageBlockRenderer from "../../components/blocks/ImageBlockRenderer";
|
|
50
|
+
import ImageGridBlockRenderer from "../../components/blocks/ImageGridBlockRenderer";
|
|
51
|
+
import VideoBlockRenderer from "../../components/blocks/VideoBlockRenderer";
|
|
52
|
+
import SpacerBlockRenderer from "../../components/blocks/SpacerBlockRenderer";
|
|
53
|
+
import ButtonBlockRenderer from "../../components/blocks/ButtonBlockRenderer";
|
|
54
|
+
import ProjectGridBlockRenderer from "../../components/blocks/ProjectGridBlockRenderer";
|
|
55
|
+
import ProjectCarouselBlockRenderer from "../../components/blocks/ProjectCarouselBlockRenderer";
|
|
56
|
+
|
|
57
|
+
// ── Builder live previews ─────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
import LiveTextEditor from "../../components/builder/live-preview/LiveTextEditor";
|
|
60
|
+
import LiveImagePreview from "../../components/builder/live-preview/LiveImagePreview";
|
|
61
|
+
import LiveImageGridPreview from "../../components/builder/live-preview/LiveImageGridPreview";
|
|
62
|
+
import LiveVideoPreview from "../../components/builder/live-preview/LiveVideoPreview";
|
|
63
|
+
import LiveSpacerPreview from "../../components/builder/live-preview/LiveSpacerPreview";
|
|
64
|
+
import LiveButtonPreview from "../../components/builder/live-preview/LiveButtonPreview";
|
|
65
|
+
import LiveProjectGridPreview from "../../components/builder/live-preview/LiveProjectGridPreview";
|
|
66
|
+
import LiveProjectCarouselPreview from "../../components/builder/live-preview/LiveProjectCarouselPreview";
|
|
67
|
+
|
|
68
|
+
// ── Settings-panel editors ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
import TextBlockEditor from "../../components/builder/editors/TextBlockEditor";
|
|
71
|
+
import ImageBlockEditor from "../../components/builder/editors/ImageBlockEditor";
|
|
72
|
+
import ImageGridBlockEditor from "../../components/builder/editors/ImageGridBlockEditor";
|
|
73
|
+
import VideoBlockEditor from "../../components/builder/editors/VideoBlockEditor";
|
|
74
|
+
import SpacerBlockEditor from "../../components/builder/editors/SpacerBlockEditor";
|
|
75
|
+
import ButtonBlockEditor from "../../components/builder/editors/ButtonBlockEditor";
|
|
76
|
+
import ProjectGridEditor from "../../components/builder/editors/ProjectGridEditor";
|
|
77
|
+
import ProjectCarouselBlockEditor from "../../components/builder/editors/ProjectCarouselBlockEditor";
|
|
78
|
+
|
|
79
|
+
// ── Card icons (picker modal) ─────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
import {
|
|
82
|
+
TextBlockCardIcon,
|
|
83
|
+
ImageBlockCardIcon,
|
|
84
|
+
ImageGridBlockCardIcon,
|
|
85
|
+
VideoBlockCardIcon,
|
|
86
|
+
SpacerBlockCardIcon,
|
|
87
|
+
ButtonBlockCardIcon,
|
|
88
|
+
} from "../../components/builder/BlockCardIcons";
|
|
89
|
+
import {
|
|
90
|
+
ProjectGridCardIcon,
|
|
91
|
+
ProjectCarouselCardIcon,
|
|
92
|
+
} from "../../components/builder/SectionCardIcons";
|
|
93
|
+
|
|
94
|
+
// ── Compact icons (settings panel header) ─────────────────────────
|
|
95
|
+
|
|
96
|
+
import {
|
|
97
|
+
TextBlockIcon,
|
|
98
|
+
ImageBlockIcon,
|
|
99
|
+
ImageGridBlockIcon,
|
|
100
|
+
VideoBlockIcon,
|
|
101
|
+
SpacerBlockIcon,
|
|
102
|
+
ButtonBlockIcon,
|
|
103
|
+
ProjectGridBlockIcon,
|
|
104
|
+
ProjectCarouselBlockIcon,
|
|
105
|
+
} from "../../components/builder/blockStyles";
|
|
106
|
+
|
|
107
|
+
// ────────────────────────────────────────────────────────────────────
|
|
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.
|
|
117
|
+
// ────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
// ── Content blocks ──
|
|
120
|
+
|
|
121
|
+
registerBlockType<TextBlock>({
|
|
122
|
+
type: "textBlock",
|
|
123
|
+
label: "Text",
|
|
124
|
+
description: "Rich text content",
|
|
125
|
+
category: "content",
|
|
126
|
+
iconGlyph: "T",
|
|
127
|
+
schema: textBlock,
|
|
128
|
+
defaultFactory: (key) => ({
|
|
129
|
+
_type: "textBlock",
|
|
130
|
+
_key: key,
|
|
131
|
+
text: [],
|
|
132
|
+
style: { fontSize: 14, alignment: "left", fontWeight: "400" },
|
|
133
|
+
}),
|
|
134
|
+
renderer: TextBlockRenderer as React.ComponentType<{ block: TextBlock }>,
|
|
135
|
+
livePreview: LiveTextEditor as unknown as React.ComponentType<{ block: TextBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
136
|
+
editor: TextBlockEditor as React.ComponentType<{ block: TextBlock }>,
|
|
137
|
+
cardIcon: TextBlockCardIcon,
|
|
138
|
+
compactIcon: TextBlockIcon,
|
|
139
|
+
enterPresets: ["typewriter", "fade", "blur-in", "slide-up"],
|
|
140
|
+
hoverPresets: [],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
registerBlockType<ImageBlock>({
|
|
144
|
+
type: "imageBlock",
|
|
145
|
+
label: "Image",
|
|
146
|
+
description: "Single image with caption",
|
|
147
|
+
category: "content",
|
|
148
|
+
iconGlyph: "🖼",
|
|
149
|
+
schema: imageBlock,
|
|
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
|
+
}),
|
|
161
|
+
renderer: ImageBlockRenderer as React.ComponentType<{ block: ImageBlock }>,
|
|
162
|
+
livePreview: LiveImagePreview as unknown as React.ComponentType<{ block: ImageBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
163
|
+
editor: ImageBlockEditor as React.ComponentType<{ block: ImageBlock }>,
|
|
164
|
+
cardIcon: ImageBlockCardIcon,
|
|
165
|
+
compactIcon: ImageBlockIcon,
|
|
166
|
+
enterPresets: ["fade", "blur", "reveal", "scale", "slide-up"],
|
|
167
|
+
hoverPresets: ["scale-up", "lift", "tilt-3d", "ripple", "rgb-shift", "pixelate"],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
registerBlockType<ImageGridBlock>({
|
|
171
|
+
type: "imageGridBlock",
|
|
172
|
+
label: "Image Grid",
|
|
173
|
+
description: "Multiple images in grid",
|
|
174
|
+
category: "content",
|
|
175
|
+
iconGlyph: "⊞",
|
|
176
|
+
schema: imageGridBlock,
|
|
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
|
+
}),
|
|
189
|
+
renderer: ImageGridBlockRenderer as React.ComponentType<{ block: ImageGridBlock }>,
|
|
190
|
+
livePreview: LiveImageGridPreview as unknown as React.ComponentType<{ block: ImageGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
191
|
+
editor: ImageGridBlockEditor as React.ComponentType<{ block: ImageGridBlock }>,
|
|
192
|
+
cardIcon: ImageGridBlockCardIcon,
|
|
193
|
+
compactIcon: ImageGridBlockIcon,
|
|
194
|
+
enterPresets: ["fade", "scale", "slide-up"],
|
|
195
|
+
hoverPresets: ["tilt-3d"],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
registerBlockType<VideoBlock>({
|
|
199
|
+
type: "videoBlock",
|
|
200
|
+
label: "Video",
|
|
201
|
+
description: "Vimeo, YouTube, or MP4",
|
|
202
|
+
category: "content",
|
|
203
|
+
iconGlyph: "▶",
|
|
204
|
+
schema: videoBlock,
|
|
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
|
+
}),
|
|
216
|
+
renderer: VideoBlockRenderer as React.ComponentType<{ block: VideoBlock }>,
|
|
217
|
+
livePreview: LiveVideoPreview as unknown as React.ComponentType<{ block: VideoBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
218
|
+
editor: VideoBlockEditor as React.ComponentType<{ block: VideoBlock }>,
|
|
219
|
+
cardIcon: VideoBlockCardIcon,
|
|
220
|
+
compactIcon: VideoBlockIcon,
|
|
221
|
+
enterPresets: ["fade", "slide-up"],
|
|
222
|
+
hoverPresets: [],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
registerBlockType<SpacerBlock>({
|
|
226
|
+
type: "spacerBlock",
|
|
227
|
+
label: "Spacer",
|
|
228
|
+
description: "Vertical spacing",
|
|
229
|
+
category: "content",
|
|
230
|
+
iconGlyph: "↕",
|
|
231
|
+
schema: spacerBlock,
|
|
232
|
+
defaultFactory: (key) => ({
|
|
233
|
+
_type: "spacerBlock",
|
|
234
|
+
_key: key,
|
|
235
|
+
height: "medium",
|
|
236
|
+
}),
|
|
237
|
+
renderer: SpacerBlockRenderer as React.ComponentType<{ block: SpacerBlock }>,
|
|
238
|
+
livePreview: LiveSpacerPreview as unknown as React.ComponentType<{ block: SpacerBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
239
|
+
editor: SpacerBlockEditor as React.ComponentType<{ block: SpacerBlock }>,
|
|
240
|
+
cardIcon: SpacerBlockCardIcon,
|
|
241
|
+
compactIcon: SpacerBlockIcon,
|
|
242
|
+
enterPresets: [],
|
|
243
|
+
hoverPresets: [],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
registerBlockType<ButtonBlock>({
|
|
247
|
+
type: "buttonBlock",
|
|
248
|
+
label: "Button",
|
|
249
|
+
description: "Call-to-action button",
|
|
250
|
+
category: "content",
|
|
251
|
+
iconGlyph: "▣",
|
|
252
|
+
schema: buttonBlock,
|
|
253
|
+
defaultFactory: (key) => ({
|
|
254
|
+
_type: "buttonBlock",
|
|
255
|
+
_key: key,
|
|
256
|
+
text: "Button",
|
|
257
|
+
url: "#",
|
|
258
|
+
style: "primary",
|
|
259
|
+
size: "medium",
|
|
260
|
+
}),
|
|
261
|
+
renderer: ButtonBlockRenderer as React.ComponentType<{ block: ButtonBlock }>,
|
|
262
|
+
livePreview: LiveButtonPreview as unknown as React.ComponentType<{ block: ButtonBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
263
|
+
editor: ButtonBlockEditor as React.ComponentType<{ block: ButtonBlock }>,
|
|
264
|
+
cardIcon: ButtonBlockCardIcon,
|
|
265
|
+
compactIcon: ButtonBlockIcon,
|
|
266
|
+
enterPresets: ["fade", "slide-up", "scale"],
|
|
267
|
+
hoverPresets: ["scale-up", "lift", "border-glow"],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ── Section-level blocks ──
|
|
271
|
+
|
|
272
|
+
registerBlockType<ProjectGridBlock>({
|
|
273
|
+
type: "projectGridBlock",
|
|
274
|
+
label: "Project Grid",
|
|
275
|
+
description: "Staggered project showcase grid",
|
|
276
|
+
category: "section",
|
|
277
|
+
iconGlyph: "⬡",
|
|
278
|
+
schema: projectGridBlock,
|
|
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
|
+
}),
|
|
292
|
+
renderer: ProjectGridBlockRenderer as React.ComponentType<{ block: ProjectGridBlock }>,
|
|
293
|
+
livePreview: LiveProjectGridPreview as unknown as React.ComponentType<{ block: ProjectGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
294
|
+
editor: ProjectGridEditor as React.ComponentType<{ block: ProjectGridBlock }>,
|
|
295
|
+
cardIcon: ProjectGridCardIcon,
|
|
296
|
+
compactIcon: ProjectGridBlockIcon,
|
|
297
|
+
enterPresets: [],
|
|
298
|
+
hoverPresets: ["scale-up", "lift"],
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
registerBlockType<ProjectCarouselBlock>({
|
|
302
|
+
type: "projectCarouselBlock",
|
|
303
|
+
label: "Project Carousel",
|
|
304
|
+
description: "Horizontal carousel of projects — great for end-of-page 'keep browsing'",
|
|
305
|
+
category: "section",
|
|
306
|
+
iconGlyph: "▸",
|
|
307
|
+
schema: projectCarouselBlock,
|
|
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
|
+
}),
|
|
328
|
+
renderer: ProjectCarouselBlockRenderer as React.ComponentType<{ block: ProjectCarouselBlock }>,
|
|
329
|
+
livePreview: LiveProjectCarouselPreview as unknown as React.ComponentType<{ block: ProjectCarouselBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
|
|
330
|
+
editor: ProjectCarouselBlockEditor as React.ComponentType<{ block: ProjectCarouselBlock }>,
|
|
331
|
+
cardIcon: ProjectCarouselCardIcon,
|
|
332
|
+
compactIcon: ProjectCarouselBlockIcon,
|
|
333
|
+
enterPresets: [],
|
|
334
|
+
hoverPresets: [],
|
|
335
|
+
});
|