@morphika/andami 0.2.12 → 0.2.14
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/app/admin/pages/[slug]/page.tsx +39 -2
- package/components/blocks/BlockRenderer.tsx +0 -7
- package/components/blocks/CoverSectionRenderer.tsx +295 -0
- package/components/blocks/ImageBlockRenderer.tsx +12 -10
- package/components/blocks/PageRenderer.tsx +13 -9
- package/components/blocks/VideoBlockRenderer.tsx +11 -6
- package/components/builder/BlockLivePreview.tsx +0 -5
- package/components/builder/BlockTypePicker.tsx +0 -1
- package/components/builder/ColorSwatchPicker.tsx +2 -2
- package/components/builder/CoverRowResizeHandle.tsx +180 -0
- package/components/builder/CoverSectionCanvas.tsx +260 -0
- package/components/builder/ReadOnlyFrame.tsx +127 -3
- package/components/builder/SectionTypePicker.tsx +29 -0
- package/components/builder/SectionV2Canvas.tsx +4 -1
- package/components/builder/SectionV2Column.tsx +15 -20
- package/components/builder/SettingsPanel.tsx +14 -0
- package/components/builder/SortableRow.tsx +7 -21
- package/components/builder/blockStyles.tsx +13 -14
- package/components/builder/editors/ImageBlockEditor.tsx +1 -0
- package/components/builder/editors/VideoBlockEditor.tsx +1 -0
- package/components/builder/editors/index.ts +0 -1
- package/components/builder/index.ts +1 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
- package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
- package/components/builder/live-preview/RichTextEditor.tsx +23 -2
- package/components/builder/live-preview/index.ts +0 -1
- package/components/builder/settings-panel/BlockSettings.tsx +0 -7
- package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
- package/lib/animation/enter-types.ts +0 -1
- package/lib/animation/hover-effect-types.ts +0 -1
- package/lib/builder/defaults.ts +43 -22
- package/lib/builder/serializer/normalizers.ts +34 -1
- package/lib/builder/serializer/serializers.ts +39 -2
- package/lib/builder/store-blocks.ts +11 -3
- package/lib/builder/store-cover.ts +220 -0
- package/lib/builder/store-helpers.ts +81 -4
- package/lib/builder/store-sections.ts +12 -2
- package/lib/builder/store.ts +11 -2
- package/lib/builder/types.ts +15 -2
- package/lib/sanity/queries.ts +18 -4
- package/lib/sanity/types.ts +81 -45
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/imageBlock.ts +1 -0
- package/sanity/schemas/blocks/index.ts +1 -2
- package/sanity/schemas/blocks/videoBlock.ts +1 -0
- package/sanity/schemas/index.ts +5 -3
- package/sanity/schemas/objects/coverSection.ts +317 -0
- package/sanity/schemas/objects/parallaxSlide.ts +0 -1
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +0 -1
- package/components/blocks/CoverBlockRenderer.tsx +0 -261
- package/components/builder/editors/CoverBlockEditor.tsx +0 -550
- package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
- package/sanity/schemas/blocks/coverBlock.ts +0 -229
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
|
+
- **7 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button, Project Grid
|
|
11
|
+
- **Cover Sections** — Full-viewport hero sections with proportional rows, background media, and drag-to-resize
|
|
11
12
|
- **V2 Grid System** — 12-column CSS grid with push cascade engine and responsive overrides
|
|
12
13
|
- **Custom Sections** — Create reusable sections with per-instance setting overrides
|
|
13
14
|
- **Navigation Builder** — Visual 12-column grid editor with drag & drop
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
SortableContext,
|
|
19
19
|
verticalListSortingStrategy,
|
|
20
20
|
} from "@dnd-kit/sortable";
|
|
21
|
-
import type { Page, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
|
|
22
|
-
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../../lib/sanity/types";
|
|
21
|
+
import type { Page, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem, CoverSection } from "../../../../lib/sanity/types";
|
|
22
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSection } from "../../../../lib/sanity/types";
|
|
23
23
|
import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
|
|
24
24
|
import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
|
|
25
25
|
import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
|
|
@@ -29,6 +29,7 @@ import { isSectionBlockType } from "../../../../lib/builder/types";
|
|
|
29
29
|
import BlockLivePreview from "../../../../components/builder/BlockLivePreview";
|
|
30
30
|
import SectionV2Canvas from "../../../../components/builder/SectionV2Canvas";
|
|
31
31
|
import ParallaxGroupCanvas from "../../../../components/builder/ParallaxGroupCanvas";
|
|
32
|
+
import CoverSectionCanvas from "../../../../components/builder/CoverSectionCanvas";
|
|
32
33
|
import { ThumbStatusProvider } from "../../../../lib/contexts/ThumbStatusContext";
|
|
33
34
|
import PublishToggle from "../../../../components/admin/PublishToggle";
|
|
34
35
|
|
|
@@ -445,6 +446,12 @@ export default function PageEditorPage() {
|
|
|
445
446
|
setShowSectionPicker(false);
|
|
446
447
|
}, [store]);
|
|
447
448
|
|
|
449
|
+
// Handle add cover section
|
|
450
|
+
const handleAddCoverSection = useCallback(() => {
|
|
451
|
+
store.addCoverSection(null);
|
|
452
|
+
setShowSectionPicker(false);
|
|
453
|
+
}, [store]);
|
|
454
|
+
|
|
448
455
|
// Handle add block — V2 sections only
|
|
449
456
|
const handleAddBlock = useCallback(
|
|
450
457
|
(type: BlockType) => {
|
|
@@ -667,6 +674,7 @@ export default function PageEditorPage() {
|
|
|
667
674
|
const isV2Section = isPageSectionV2(item);
|
|
668
675
|
const isInstance = isCustomSectionInstance(item);
|
|
669
676
|
const isParallax = isParallaxGroup(item);
|
|
677
|
+
const isCover = isCoverSection(item);
|
|
670
678
|
const v2Section = isV2Section ? (item as PageSectionV2) : null;
|
|
671
679
|
|
|
672
680
|
// Custom Section Instance — rendered directly without SortableRow chrome
|
|
@@ -722,6 +730,34 @@ export default function PageEditorPage() {
|
|
|
722
730
|
);
|
|
723
731
|
}
|
|
724
732
|
|
|
733
|
+
// Cover Section — full-viewport with proportional rows
|
|
734
|
+
if (isCover) {
|
|
735
|
+
const cover = item as CoverSection;
|
|
736
|
+
return (
|
|
737
|
+
<SortableRow
|
|
738
|
+
key={item._key}
|
|
739
|
+
rowKey={item._key}
|
|
740
|
+
row={item}
|
|
741
|
+
isSelected={store.selectedRowKey === item._key}
|
|
742
|
+
columnCount={0}
|
|
743
|
+
onSelect={() => store.selectRow(item._key)}
|
|
744
|
+
onDelete={() => store.deleteSection(item._key)}
|
|
745
|
+
onAddColumn={() => store.addCoverRow(item._key)}
|
|
746
|
+
addColumnLabel="Row"
|
|
747
|
+
onDuplicate={() => store.duplicateSection(item._key)}
|
|
748
|
+
onMoveUp={() => { if (rowIndex > 0) store.reorderRows(rowIndex, rowIndex - 1); }}
|
|
749
|
+
onMoveDown={() => { if (rowIndex < store.rows.length - 1) store.reorderRows(rowIndex, rowIndex + 1); }}
|
|
750
|
+
isFirst={rowIndex === 0}
|
|
751
|
+
isLast={rowIndex === store.rows.length - 1}
|
|
752
|
+
>
|
|
753
|
+
<CoverSectionCanvas
|
|
754
|
+
section={cover}
|
|
755
|
+
onAddBlockTarget={handleAddBlockTargetV2}
|
|
756
|
+
/>
|
|
757
|
+
</SortableRow>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
725
761
|
return (
|
|
726
762
|
<SortableRow
|
|
727
763
|
key={item._key}
|
|
@@ -810,6 +846,7 @@ export default function PageEditorPage() {
|
|
|
810
846
|
onSelectEmptyV2={handleAddEmptySectionV2}
|
|
811
847
|
onSelectSection={handleAddSection}
|
|
812
848
|
onSelectParallaxGroup={handleAddParallaxGroup}
|
|
849
|
+
onSelectCoverSection={handleAddCoverSection}
|
|
813
850
|
onSelectCustomSection={handleSelectCustomSection}
|
|
814
851
|
onCreateCustomSection={handleCreateCustomSection}
|
|
815
852
|
onClose={() => setShowSectionPicker(false)}
|
|
@@ -23,7 +23,6 @@ import ImageGridBlockRenderer from "./ImageGridBlockRenderer";
|
|
|
23
23
|
import VideoBlockRenderer from "./VideoBlockRenderer";
|
|
24
24
|
import SpacerBlockRenderer from "./SpacerBlockRenderer";
|
|
25
25
|
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
26
|
-
import CoverBlockRenderer from "./CoverBlockRenderer";
|
|
27
26
|
import ProjectGridBlockRenderer from "./ProjectGridBlockRenderer";
|
|
28
27
|
|
|
29
28
|
// ── BLK-003: Error Boundary for block renderers ──
|
|
@@ -308,9 +307,6 @@ export default function BlockRenderer({
|
|
|
308
307
|
case "buttonBlock":
|
|
309
308
|
content = <ButtonBlockRenderer block={resolved} />;
|
|
310
309
|
break;
|
|
311
|
-
case "coverBlock":
|
|
312
|
-
content = <CoverBlockRenderer block={resolved} />;
|
|
313
|
-
break;
|
|
314
310
|
case "projectGridBlock":
|
|
315
311
|
content = <ProjectGridBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectGridBlock} />;
|
|
316
312
|
break;
|
|
@@ -421,9 +417,6 @@ export default function BlockRenderer({
|
|
|
421
417
|
shaderSrc = resolveAsset((resolved as import("../../lib/sanity/types").ImageBlock).asset_path);
|
|
422
418
|
const br = (resolved as import("../../lib/sanity/types").ImageBlock).border_radius;
|
|
423
419
|
if (br) shaderBorderRadius = `${String(br).replace(/px$/i, "")}px`;
|
|
424
|
-
} else if (resolved._type === "coverBlock") {
|
|
425
|
-
const mediaPath = (resolved as import("../../lib/sanity/types").CoverBlock).media_path;
|
|
426
|
-
if (mediaPath) shaderSrc = resolveAsset(mediaPath);
|
|
427
420
|
}
|
|
428
421
|
// Shader preset without image src: skip wrapper entirely
|
|
429
422
|
if (!shaderSrc) {
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CoverSectionRenderer — renders a CoverSection on the public site.
|
|
5
|
+
*
|
|
6
|
+
* Fixed-height viewport section with background image/video, overlay,
|
|
7
|
+
* and proportional CSS Grid rows. Columns and blocks use the same
|
|
8
|
+
* rendering pipeline as SectionV2Renderer.
|
|
9
|
+
*
|
|
10
|
+
* Session 176: Cover Sections — Phase 9 (Public Renderer).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CoverSection,
|
|
15
|
+
SectionColumn,
|
|
16
|
+
ContentBlock,
|
|
17
|
+
BlockLayout,
|
|
18
|
+
} from "../../lib/sanity/types";
|
|
19
|
+
import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
|
|
20
|
+
import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
|
|
21
|
+
import BlockRenderer from "./BlockRenderer";
|
|
22
|
+
import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
23
|
+
import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
24
|
+
import { assetUrl } from "../../lib/assets";
|
|
25
|
+
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
26
|
+
|
|
27
|
+
interface CoverSectionRendererProps {
|
|
28
|
+
section: CoverSection;
|
|
29
|
+
pageEnterAnimation?: EnterAnimationConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildCoverResponsiveCss(section: CoverSection): string | null {
|
|
33
|
+
const responsive = section.responsive;
|
|
34
|
+
if (!responsive) return null;
|
|
35
|
+
|
|
36
|
+
const key = section._key;
|
|
37
|
+
const cssParts: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
|
|
40
|
+
const override = responsive[vp];
|
|
41
|
+
if (!override) continue;
|
|
42
|
+
|
|
43
|
+
const rules: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (override.settings?.col_gap !== undefined) {
|
|
46
|
+
rules.push(`column-gap:${override.settings.col_gap}px!important`);
|
|
47
|
+
}
|
|
48
|
+
if (override.settings?.row_gap !== undefined) {
|
|
49
|
+
rules.push(`row-gap:${override.settings.row_gap}px!important`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (override.cover_rows && override.cover_rows.length > 0) {
|
|
53
|
+
const baseRows = section.cover_rows;
|
|
54
|
+
const rowTemplate = baseRows.map((r, i) => {
|
|
55
|
+
const rowOverride = override.cover_rows?.find((o) => o._key === r._key);
|
|
56
|
+
return `${rowOverride?.height_percent ?? r.height_percent}%`;
|
|
57
|
+
}).join(" ");
|
|
58
|
+
rules.push(`grid-template-rows:${rowTemplate}!important`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rules.length > 0) {
|
|
62
|
+
cssParts.push(`@media(max-width:${breakpoint}px){.cover-grid-${key}{${rules.join(";")}}}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (override.columns) {
|
|
66
|
+
for (const co of override.columns) {
|
|
67
|
+
const colRules: string[] = [];
|
|
68
|
+
if (co.grid_column !== undefined && co.span !== undefined) {
|
|
69
|
+
colRules.push(`grid-column:${co.grid_column}/span ${co.span}!important`);
|
|
70
|
+
}
|
|
71
|
+
if (co.grid_row !== undefined) {
|
|
72
|
+
colRules.push(`grid-row:${co.grid_row}!important`);
|
|
73
|
+
}
|
|
74
|
+
if (colRules.length > 0) {
|
|
75
|
+
cssParts.push(`@media(max-width:${breakpoint}px){.cover-col-${key}-${co._key}{${colRules.join(";")}}}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return cssParts.length > 0 ? cssParts.join("") : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function CoverSectionRenderer({ section, pageEnterAnimation }: CoverSectionRendererProps) {
|
|
85
|
+
const s = section.settings;
|
|
86
|
+
const gridColumns = s.grid_columns || 12;
|
|
87
|
+
const colGap = s.col_gap ?? 20;
|
|
88
|
+
const rowGap = s.row_gap ?? 20;
|
|
89
|
+
|
|
90
|
+
const rowTemplate = section.cover_rows
|
|
91
|
+
.map((r) => `${r.height_percent}%`)
|
|
92
|
+
.join(" ");
|
|
93
|
+
|
|
94
|
+
const sectionEnterConfig = s.enter_animation;
|
|
95
|
+
const resolvedSectionEnter = resolveEnterAnimation(undefined, undefined, sectionEnterConfig, pageEnterAnimation);
|
|
96
|
+
const hasAnimation = resolvedSectionEnter !== null && resolvedSectionEnter.preset !== "none";
|
|
97
|
+
|
|
98
|
+
const stagger = s.stagger;
|
|
99
|
+
const staggerEnabled = stagger?.enabled && hasAnimation;
|
|
100
|
+
const staggerDelay = stagger?.delayPerChild ?? 100;
|
|
101
|
+
const staggerDirection = stagger?.direction ?? "left-to-right";
|
|
102
|
+
|
|
103
|
+
const bgImageSrc = section.background_type === "image" && section.background_image
|
|
104
|
+
? assetUrl(section.background_image)
|
|
105
|
+
: null;
|
|
106
|
+
const bgVideoSrc = section.background_type === "video" && section.background_video
|
|
107
|
+
? assetUrl(section.background_video)
|
|
108
|
+
: null;
|
|
109
|
+
const overlayOpacity = section.background_overlay_opacity ?? 0;
|
|
110
|
+
|
|
111
|
+
const responsiveCss = buildCoverResponsiveCss(section);
|
|
112
|
+
|
|
113
|
+
const sortedColumns = [...section.columns].sort((a, b) => {
|
|
114
|
+
if (a.grid_row !== b.grid_row) return a.grid_row - b.grid_row;
|
|
115
|
+
return a.grid_column - b.grid_column;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const getStaggerIndex = (colIndex: number): number => {
|
|
119
|
+
if (!staggerEnabled) return 0;
|
|
120
|
+
return staggerDirection === "right-to-left"
|
|
121
|
+
? sortedColumns.length - 1 - colIndex
|
|
122
|
+
: colIndex;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const rowAlignMap: Record<string, string> = {};
|
|
126
|
+
section.cover_rows.forEach((row, i) => {
|
|
127
|
+
rowAlignMap[String(i + 1)] = row.vertical_align || "start";
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const sectionContent = (
|
|
131
|
+
<section
|
|
132
|
+
style={{
|
|
133
|
+
position: "relative",
|
|
134
|
+
height: section.height,
|
|
135
|
+
overflow: "hidden",
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
|
|
139
|
+
|
|
140
|
+
{/* Background image */}
|
|
141
|
+
{bgImageSrc && (
|
|
142
|
+
<div
|
|
143
|
+
style={{
|
|
144
|
+
position: "absolute",
|
|
145
|
+
inset: 0,
|
|
146
|
+
backgroundImage: `url(${bgImageSrc})`,
|
|
147
|
+
backgroundSize: section.background_size || "cover",
|
|
148
|
+
backgroundPosition: section.background_position || "center center",
|
|
149
|
+
backgroundRepeat: "no-repeat",
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Background video */}
|
|
155
|
+
{bgVideoSrc && (
|
|
156
|
+
<video
|
|
157
|
+
autoPlay
|
|
158
|
+
muted
|
|
159
|
+
loop
|
|
160
|
+
playsInline
|
|
161
|
+
style={{
|
|
162
|
+
position: "absolute",
|
|
163
|
+
inset: 0,
|
|
164
|
+
width: "100%",
|
|
165
|
+
height: "100%",
|
|
166
|
+
objectFit: "cover",
|
|
167
|
+
objectPosition: section.background_position || "center center",
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<source src={bgVideoSrc} />
|
|
171
|
+
</video>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Overlay */}
|
|
175
|
+
{overlayOpacity > 0 && (
|
|
176
|
+
<div
|
|
177
|
+
style={{
|
|
178
|
+
position: "absolute",
|
|
179
|
+
inset: 0,
|
|
180
|
+
backgroundColor: section.background_overlay_color || "#000000",
|
|
181
|
+
opacity: overlayOpacity / 100,
|
|
182
|
+
pointerEvents: "none",
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Content grid */}
|
|
188
|
+
<div
|
|
189
|
+
className={`cover-grid-${section._key}`}
|
|
190
|
+
style={{
|
|
191
|
+
position: "relative",
|
|
192
|
+
zIndex: 1,
|
|
193
|
+
display: "grid",
|
|
194
|
+
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
|
195
|
+
gridTemplateRows: rowTemplate,
|
|
196
|
+
height: "100%",
|
|
197
|
+
columnGap: `${colGap}px`,
|
|
198
|
+
rowGap: `${rowGap}px`,
|
|
199
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
200
|
+
marginLeft: "auto",
|
|
201
|
+
marginRight: "auto",
|
|
202
|
+
width: "100%",
|
|
203
|
+
padding: [
|
|
204
|
+
s.spacing_top ? `${s.spacing_top}px` : "0",
|
|
205
|
+
s.spacing_right ? `${s.spacing_right}px` : "0",
|
|
206
|
+
s.spacing_bottom ? `${s.spacing_bottom}px` : "0",
|
|
207
|
+
s.spacing_left ? `${s.spacing_left}px` : "0",
|
|
208
|
+
].join(" "),
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{sortedColumns.map((col, colIndex) => {
|
|
212
|
+
const staggerIdx = staggerEnabled ? getStaggerIndex(colIndex) : undefined;
|
|
213
|
+
const rowAlign = rowAlignMap[String(col.grid_row)] || "start";
|
|
214
|
+
const alignSelf = rowAlign === "center" ? "center" : rowAlign === "end" ? "end" : "start";
|
|
215
|
+
const colJustify = getColumnVerticalAlign(col.blocks || []);
|
|
216
|
+
|
|
217
|
+
const columnContent = (
|
|
218
|
+
<div
|
|
219
|
+
key={col._key}
|
|
220
|
+
className={`cover-col-${section._key}-${col._key}`}
|
|
221
|
+
style={{
|
|
222
|
+
gridColumn: `${col.grid_column} / span ${col.span}`,
|
|
223
|
+
gridRow: col.grid_row,
|
|
224
|
+
alignSelf,
|
|
225
|
+
minWidth: 0,
|
|
226
|
+
overflow: "hidden",
|
|
227
|
+
...(colJustify ? { display: "flex", flexDirection: "column" as const, justifyContent: colJustify } : {}),
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{(col.blocks || []).map((block) => {
|
|
231
|
+
const blockEnter = resolveEnterAnimation(
|
|
232
|
+
block.enter_animation as EnterAnimationConfig | undefined,
|
|
233
|
+
col.enter_animation,
|
|
234
|
+
sectionEnterConfig,
|
|
235
|
+
pageEnterAnimation
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const layout = (block as ContentBlock & { layout?: BlockLayout }).layout;
|
|
239
|
+
const alignStyles = layout && hasBlockAlignment(layout) ? getBlockAlignmentStyles(layout) : {};
|
|
240
|
+
|
|
241
|
+
const rendered = (
|
|
242
|
+
<div key={block._key} style={alignStyles}>
|
|
243
|
+
<BlockRenderer block={block} />
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (blockEnter && blockEnter.preset !== "none") {
|
|
248
|
+
return (
|
|
249
|
+
<EnterAnimationWrapper key={block._key} config={blockEnter}>
|
|
250
|
+
{rendered}
|
|
251
|
+
</EnterAnimationWrapper>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return rendered;
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (staggerEnabled && staggerIdx !== undefined) {
|
|
260
|
+
const colEnter = resolveEnterAnimation(
|
|
261
|
+
col.enter_animation,
|
|
262
|
+
undefined,
|
|
263
|
+
sectionEnterConfig,
|
|
264
|
+
pageEnterAnimation
|
|
265
|
+
);
|
|
266
|
+
if (colEnter && colEnter.preset !== "none") {
|
|
267
|
+
return (
|
|
268
|
+
<EnterAnimationWrapper
|
|
269
|
+
key={col._key}
|
|
270
|
+
config={colEnter}
|
|
271
|
+
staggerIndex={staggerIdx}
|
|
272
|
+
staggerDelay={staggerDelay}
|
|
273
|
+
>
|
|
274
|
+
{columnContent}
|
|
275
|
+
</EnterAnimationWrapper>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return columnContent;
|
|
281
|
+
})}
|
|
282
|
+
</div>
|
|
283
|
+
</section>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (resolvedSectionEnter && resolvedSectionEnter.preset !== "none" && !staggerEnabled) {
|
|
287
|
+
return (
|
|
288
|
+
<EnterAnimationWrapper config={resolvedSectionEnter}>
|
|
289
|
+
{sectionContent}
|
|
290
|
+
</EnterAnimationWrapper>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return sectionContent;
|
|
295
|
+
}
|
|
@@ -25,24 +25,26 @@ const aspectMap: Record<string, string | undefined> = {
|
|
|
25
25
|
export default function ImageBlockRenderer({ block }: { block: ImageBlock }) {
|
|
26
26
|
const resolveAsset = useAssetUrl();
|
|
27
27
|
const src = resolveAsset(block.asset_path);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const isFill = block.width === "fill";
|
|
29
|
+
const widthStyle = isFill ? {} : (widthStyleMap[block.width ?? "full"] || widthStyleMap.full);
|
|
30
|
+
const aspect = isFill ? undefined : aspectMap[block.aspect_ratio ?? "auto"];
|
|
30
31
|
|
|
31
32
|
// BLK-014: Strip any existing unit suffix, then validate as a number before appending px
|
|
32
33
|
const rawRadius = block.border_radius ? String(block.border_radius).replace(/[a-z%]+$/i, "") : "";
|
|
33
34
|
const borderRadius = rawRadius && !isNaN(Number(rawRadius)) ? `${rawRadius}px` : undefined;
|
|
34
35
|
|
|
35
|
-
const imgStyle: React.CSSProperties =
|
|
36
|
-
width: "100%",
|
|
37
|
-
display: "block",
|
|
38
|
-
objectFit: aspect ? "cover" : undefined,
|
|
39
|
-
aspectRatio: aspect,
|
|
40
|
-
};
|
|
36
|
+
const imgStyle: React.CSSProperties = isFill
|
|
37
|
+
? { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }
|
|
38
|
+
: { width: "100%", display: "block", objectFit: aspect ? "cover" : undefined, aspectRatio: aspect };
|
|
41
39
|
|
|
42
40
|
const imgClassName = block.shadow ? "shadow-lg" : "";
|
|
43
41
|
|
|
42
|
+
const figureStyle: React.CSSProperties = isFill
|
|
43
|
+
? { position: "absolute", inset: 0, borderRadius, overflow: "hidden" }
|
|
44
|
+
: { ...widthStyle, borderRadius, overflow: "hidden" };
|
|
45
|
+
|
|
44
46
|
return (
|
|
45
|
-
<figure style={
|
|
47
|
+
<figure style={figureStyle}>
|
|
46
48
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
47
49
|
<img
|
|
48
50
|
src={src}
|
|
@@ -53,7 +55,7 @@ export default function ImageBlockRenderer({ block }: { block: ImageBlock }) {
|
|
|
53
55
|
style={imgStyle}
|
|
54
56
|
className={imgClassName}
|
|
55
57
|
/>
|
|
56
|
-
{block.caption && (
|
|
58
|
+
{!isFill && block.caption && (
|
|
57
59
|
<figcaption className="mt-2 font-sans text-xs uppercase tracking-wider text-brand-muted">
|
|
58
60
|
{block.caption}
|
|
59
61
|
</figcaption>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Page, ContentBlock, ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
|
|
2
|
-
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
1
|
+
import type { Page, ContentBlock, ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup, CoverSection } from "../../lib/sanity/types";
|
|
2
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
3
3
|
import SectionV2Renderer from "./SectionV2Renderer";
|
|
4
4
|
import CustomSectionInstanceRenderer from "./CustomSectionInstanceRenderer";
|
|
5
5
|
import ParallaxGroupRenderer from "./ParallaxGroupRenderer";
|
|
6
|
+
import CoverSectionRenderer from "./CoverSectionRenderer";
|
|
6
7
|
import { PageNavColor } from "./PageNavColor";
|
|
7
8
|
import { PageNavAnimation } from "./PageNavAnimation";
|
|
8
9
|
import { PageBackground } from "./PageBackground";
|
|
@@ -14,14 +15,8 @@ import { parseColorField, colorToCSSProperty } from "../../lib/color-utils";
|
|
|
14
15
|
* Used to inject <link rel="preload"> for the above-the-fold hero image,
|
|
15
16
|
* reducing LCP by 200–500ms on mobile.
|
|
16
17
|
*/
|
|
17
|
-
/** Check a single block for an image
|
|
18
|
+
/** Check a single block for an image path */
|
|
18
19
|
function getBlockImagePath(block: ContentBlock): string | null {
|
|
19
|
-
if (block._type === "coverBlock") {
|
|
20
|
-
const cover = block as Extract<ContentBlock, { _type: "coverBlock" }>;
|
|
21
|
-
if (cover.media_type !== "video" && cover.media_path) {
|
|
22
|
-
return cover.media_path;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
20
|
if (block._type === "imageBlock") {
|
|
26
21
|
const img = block as Extract<ContentBlock, { _type: "imageBlock" }>;
|
|
27
22
|
if (img.asset_path) {
|
|
@@ -51,6 +46,13 @@ function findFirstImagePath(items: ContentItem[]): string | null {
|
|
|
51
46
|
}
|
|
52
47
|
break;
|
|
53
48
|
}
|
|
49
|
+
if (isCoverSection(item)) {
|
|
50
|
+
const cover = item as CoverSection;
|
|
51
|
+
if (cover.background_type !== "video" && cover.background_image) {
|
|
52
|
+
return cover.background_image;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
54
56
|
if (isPageSectionV2(item)) {
|
|
55
57
|
// Check inside V2 section columns for image/cover blocks
|
|
56
58
|
const section = item as PageSectionV2;
|
|
@@ -119,6 +121,8 @@ export default function PageRenderer({ page }: { page: Page }) {
|
|
|
119
121
|
{page.content_rows.map((item) =>
|
|
120
122
|
isCustomSectionInstance(item)
|
|
121
123
|
? <CustomSectionInstanceRenderer key={item._key} instance={item as CustomSectionInstance} />
|
|
124
|
+
: isCoverSection(item)
|
|
125
|
+
? <CoverSectionRenderer key={item._key} section={item as CoverSection} pageEnterAnimation={pageEnterAnimation} />
|
|
122
126
|
: isParallaxGroup(item)
|
|
123
127
|
? <ParallaxGroupRenderer key={item._key} group={item as ParallaxGroup} pageEnterAnimation={pageEnterAnimation} />
|
|
124
128
|
: isPageSectionV2(item)
|
|
@@ -286,18 +286,23 @@ function NativeVideo({ block, paddingBottom, resolveAsset }: {
|
|
|
286
286
|
|
|
287
287
|
export default function VideoBlockRenderer({ block }: { block: VideoBlock }) {
|
|
288
288
|
const resolveAsset = useAssetUrl();
|
|
289
|
-
const
|
|
290
|
-
const
|
|
289
|
+
const isFill = block.width === "fill";
|
|
290
|
+
const widthStyle = isFill ? {} : (widthStyleMap[block.width ?? "full"] || widthStyleMap.full);
|
|
291
|
+
const paddingBottom = isFill ? "100%" : (aspectMap[block.aspect_ratio ?? "16:9"] || "56.25%");
|
|
291
292
|
const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
|
|
292
293
|
|
|
294
|
+
const containerStyle: React.CSSProperties = isFill
|
|
295
|
+
? { position: "absolute", inset: 0, borderRadius, overflow: "hidden" }
|
|
296
|
+
: { ...widthStyle, borderRadius, overflow: borderRadius ? "hidden" : undefined };
|
|
297
|
+
|
|
293
298
|
return (
|
|
294
|
-
<div style={
|
|
299
|
+
<div style={containerStyle}>
|
|
295
300
|
{block.video_type === "vimeo" ? (
|
|
296
|
-
<VimeoEmbed block={block} paddingBottom={paddingBottom} />
|
|
301
|
+
<VimeoEmbed block={block} paddingBottom={isFill ? "100%" : paddingBottom} />
|
|
297
302
|
) : block.video_type === "youtube" ? (
|
|
298
|
-
<YouTubeEmbed block={block} paddingBottom={paddingBottom} />
|
|
303
|
+
<YouTubeEmbed block={block} paddingBottom={isFill ? "100%" : paddingBottom} />
|
|
299
304
|
) : (
|
|
300
|
-
<NativeVideo block={block} paddingBottom={paddingBottom} resolveAsset={resolveAsset} />
|
|
305
|
+
<NativeVideo block={block} paddingBottom={isFill ? "100%" : paddingBottom} resolveAsset={resolveAsset} />
|
|
301
306
|
)}
|
|
302
307
|
</div>
|
|
303
308
|
);
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
VideoBlock,
|
|
23
23
|
SpacerBlock,
|
|
24
24
|
ButtonBlock,
|
|
25
|
-
CoverBlock,
|
|
26
25
|
ProjectGridBlock,
|
|
27
26
|
} from "../../lib/sanity/types";
|
|
28
27
|
|
|
@@ -32,7 +31,6 @@ import { LiveImageGridPreview } from "./live-preview";
|
|
|
32
31
|
import { LiveVideoPreview } from "./live-preview";
|
|
33
32
|
import { LiveSpacerPreview } from "./live-preview";
|
|
34
33
|
import { LiveButtonPreview } from "./live-preview";
|
|
35
|
-
import { LiveCoverPreview } from "./live-preview";
|
|
36
34
|
import { LiveProjectGridPreview } from "./live-preview";
|
|
37
35
|
import { LivePlaceholder } from "./live-preview";
|
|
38
36
|
|
|
@@ -73,9 +71,6 @@ function BlockLivePreviewInner({ block, viewport = "desktop", editable = false }
|
|
|
73
71
|
case "buttonBlock":
|
|
74
72
|
content = <LiveButtonPreview block={resolved as ButtonBlock} />;
|
|
75
73
|
break;
|
|
76
|
-
case "coverBlock":
|
|
77
|
-
content = <LiveCoverPreview block={resolved as CoverBlock} />;
|
|
78
|
-
break;
|
|
79
74
|
case "projectGridBlock":
|
|
80
75
|
content = <LiveProjectGridPreview block={resolved as ProjectGridBlock} viewport={viewport} />;
|
|
81
76
|
break;
|
|
@@ -19,7 +19,6 @@ const BLOCK_LABELS: Record<string, { label: string; description: string }> = {
|
|
|
19
19
|
videoBlock: { label: "Video", description: "Vimeo, YouTube or MP4 file" },
|
|
20
20
|
spacerBlock: { label: "Spacer", description: "Customizable vertical spacing" },
|
|
21
21
|
buttonBlock: { label: "Button", description: "Call-to-action button (CTA)" },
|
|
22
|
-
coverBlock: { label: "Cover", description: "Full-screen hero section with image/video" },
|
|
23
22
|
projectGridBlock: { label: "Project Grid", description: "Staggered project showcase grid" },
|
|
24
23
|
};
|
|
25
24
|
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* - Backward compatible: string-only usage still works (default behavior).
|
|
13
13
|
*
|
|
14
14
|
* Used in: SettingsPanel (row/block bg, border), TextBlockEditor (text color),
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* BlockLayoutTab, SectionV2LayoutTab, PageSettings, ParallaxSlideSettings,
|
|
16
|
+
* CoverSectionSettings (overlay color), and any future color field.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { useState, useCallback, useEffect } from "react";
|