@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.
Files changed (58) hide show
  1. package/README.md +2 -1
  2. package/app/admin/pages/[slug]/page.tsx +39 -2
  3. package/components/blocks/BlockRenderer.tsx +0 -7
  4. package/components/blocks/CoverSectionRenderer.tsx +295 -0
  5. package/components/blocks/ImageBlockRenderer.tsx +12 -10
  6. package/components/blocks/PageRenderer.tsx +13 -9
  7. package/components/blocks/VideoBlockRenderer.tsx +11 -6
  8. package/components/builder/BlockLivePreview.tsx +0 -5
  9. package/components/builder/BlockTypePicker.tsx +0 -1
  10. package/components/builder/ColorSwatchPicker.tsx +2 -2
  11. package/components/builder/CoverRowResizeHandle.tsx +180 -0
  12. package/components/builder/CoverSectionCanvas.tsx +260 -0
  13. package/components/builder/ReadOnlyFrame.tsx +127 -3
  14. package/components/builder/SectionTypePicker.tsx +29 -0
  15. package/components/builder/SectionV2Canvas.tsx +4 -1
  16. package/components/builder/SectionV2Column.tsx +15 -20
  17. package/components/builder/SettingsPanel.tsx +14 -0
  18. package/components/builder/SortableRow.tsx +7 -21
  19. package/components/builder/blockStyles.tsx +13 -14
  20. package/components/builder/editors/ImageBlockEditor.tsx +1 -0
  21. package/components/builder/editors/VideoBlockEditor.tsx +1 -0
  22. package/components/builder/editors/index.ts +0 -1
  23. package/components/builder/index.ts +1 -0
  24. package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
  25. package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
  26. package/components/builder/live-preview/RichTextEditor.tsx +23 -2
  27. package/components/builder/live-preview/index.ts +0 -1
  28. package/components/builder/settings-panel/BlockSettings.tsx +0 -7
  29. package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
  30. package/components/builder/settings-panel/index.ts +1 -0
  31. package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
  32. package/lib/animation/enter-types.ts +0 -1
  33. package/lib/animation/hover-effect-types.ts +0 -1
  34. package/lib/builder/defaults.ts +43 -22
  35. package/lib/builder/serializer/normalizers.ts +34 -1
  36. package/lib/builder/serializer/serializers.ts +39 -2
  37. package/lib/builder/store-blocks.ts +11 -3
  38. package/lib/builder/store-cover.ts +220 -0
  39. package/lib/builder/store-helpers.ts +81 -4
  40. package/lib/builder/store-sections.ts +12 -2
  41. package/lib/builder/store.ts +11 -2
  42. package/lib/builder/types.ts +15 -2
  43. package/lib/sanity/queries.ts +18 -4
  44. package/lib/sanity/types.ts +81 -45
  45. package/lib/version.ts +1 -1
  46. package/package.json +1 -1
  47. package/sanity/schemas/blocks/imageBlock.ts +1 -0
  48. package/sanity/schemas/blocks/index.ts +1 -2
  49. package/sanity/schemas/blocks/videoBlock.ts +1 -0
  50. package/sanity/schemas/index.ts +5 -3
  51. package/sanity/schemas/objects/coverSection.ts +317 -0
  52. package/sanity/schemas/objects/parallaxSlide.ts +0 -1
  53. package/sanity/schemas/page.ts +1 -1
  54. package/sanity/schemas/pageSectionV2.ts +0 -1
  55. package/components/blocks/CoverBlockRenderer.tsx +0 -261
  56. package/components/builder/editors/CoverBlockEditor.tsx +0 -550
  57. package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
  58. 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
- - **9 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button, Cover, Project Grid, Parallax
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 widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
29
- const aspect = aspectMap[block.aspect_ratio ?? "auto"];
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={{ ...widthStyle, borderRadius, overflow: "hidden" }}>
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/cover path */
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 widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
290
- const paddingBottom = aspectMap[block.aspect_ratio ?? "16:9"] || "56.25%";
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={{ ...widthStyle, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
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
- * CoverBlockEditor (text color), BlockLayoutTab, SectionV2LayoutTab,
16
- * PageSettings, ParallaxSlideSettings, and any future color field.
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";