@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
@@ -0,0 +1,296 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CoverSectionSettings — Settings panel for a Cover Section.
5
+ *
6
+ * Displays:
7
+ * - Background type segmented control (Image / Video)
8
+ * - AssetBrowser picker for image or video
9
+ * - Background position / size dropdowns
10
+ * - Overlay color + overlay opacity slider
11
+ * - Section height selector (100vh / 80vh / 50vh)
12
+ * - Row list with percentages and vertical align
13
+ * - Add/Remove row controls
14
+ *
15
+ * Layout and Animation tabs are delegated to SectionV2LayoutTab / SectionV2AnimationTab
16
+ * via a virtual PageSectionV2 (same pattern as parallax slides).
17
+ *
18
+ * Session 176: Cover Sections — Phase 6 (Settings Panel).
19
+ */
20
+
21
+ import { useBuilderStore } from "../../../lib/builder/store";
22
+ import type { CoverSection } from "../../../lib/sanity/types";
23
+ import {
24
+ BackgroundIcon,
25
+ OverlayIcon,
26
+ SpacingIcon,
27
+ GridGapsIcon,
28
+ } from "../editors/section-icons";
29
+ import {
30
+ SettingsField,
31
+ SettingsSection,
32
+ } from "../editors/shared";
33
+ import { AssetPathInput } from "../editors/shared";
34
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
35
+ import StaggerSettings from "../editors/StaggerSettings";
36
+
37
+ const BG_POSITION_OPTIONS = [
38
+ { value: "center center", label: "Center" },
39
+ { value: "top center", label: "Top" },
40
+ { value: "bottom center", label: "Bottom" },
41
+ { value: "center left", label: "Left" },
42
+ { value: "center right", label: "Right" },
43
+ { value: "top left", label: "Top Left" },
44
+ { value: "top right", label: "Top Right" },
45
+ { value: "bottom left", label: "Bottom Left" },
46
+ { value: "bottom right", label: "Bottom Right" },
47
+ ];
48
+
49
+ const BG_SIZE_OPTIONS = [
50
+ { value: "cover", label: "Cover" },
51
+ { value: "contain", label: "Contain" },
52
+ { value: "auto", label: "Auto" },
53
+ ];
54
+
55
+ const HEIGHT_OPTIONS = [
56
+ { value: "100vh", label: "Full Viewport (100vh)" },
57
+ { value: "80vh", label: "80% Viewport (80vh)" },
58
+ { value: "50vh", label: "50% Viewport (50vh)" },
59
+ ];
60
+
61
+ const ALIGN_OPTIONS = [
62
+ { value: "start", label: "Top" },
63
+ { value: "center", label: "Center" },
64
+ { value: "end", label: "Bottom" },
65
+ ];
66
+
67
+ const SELECT_CLASS = "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
68
+
69
+ interface CoverSectionSettingsProps {
70
+ section: CoverSection;
71
+ }
72
+
73
+ export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
74
+ const store = useBuilderStore();
75
+ const paletteSwatches = usePaletteSwatches();
76
+
77
+ const bgType = section.background_type || "image";
78
+ const bgPosition = section.background_position || "center center";
79
+ const bgSize = section.background_size || "cover";
80
+ const overlayColor = section.background_overlay_color || "#000000";
81
+ const overlayOpacity = section.background_overlay_opacity ?? 0;
82
+
83
+ const updateBg = (fields: Partial<Pick<CoverSection,
84
+ "background_type" | "background_image" | "background_video" |
85
+ "background_position" | "background_size" |
86
+ "background_overlay_color" | "background_overlay_opacity"
87
+ >>) => {
88
+ store.updateCoverBackground(section._key, fields);
89
+ };
90
+
91
+ return (
92
+ <>
93
+ {/* Background */}
94
+ <SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
95
+ <SettingsField label="Type">
96
+ <div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
97
+ {(["image", "video"] as const).map((type) => (
98
+ <button
99
+ key={type}
100
+ onClick={() => updateBg({ background_type: type })}
101
+ className={`flex-1 py-1.5 rounded-md text-[11px] font-medium transition-all ${
102
+ bgType === type
103
+ ? "bg-white text-neutral-900 shadow-sm border border-[#e5e5e5]"
104
+ : "text-neutral-400 hover:text-neutral-500"
105
+ }`}
106
+ >
107
+ {type === "image" ? "Image" : "Video"}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ </SettingsField>
112
+
113
+ <SettingsField label={bgType === "image" ? "Image" : "Video"}>
114
+ <AssetPathInput
115
+ value={bgType === "image" ? (section.background_image || "") : (section.background_video || "")}
116
+ onChange={(path) => {
117
+ if (bgType === "image") {
118
+ updateBg({ background_image: path });
119
+ } else {
120
+ updateBg({ background_video: path });
121
+ }
122
+ }}
123
+ filterType={bgType === "image" ? "image" : "video"}
124
+ placeholder={bgType === "image" ? "path/to/image.jpg" : "path/to/video.mp4"}
125
+ />
126
+ </SettingsField>
127
+
128
+ <SettingsField label="Position">
129
+ <select
130
+ value={bgPosition}
131
+ onChange={(e) => updateBg({ background_position: e.target.value })}
132
+ className={SELECT_CLASS}
133
+ >
134
+ {BG_POSITION_OPTIONS.map((opt) => (
135
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
136
+ ))}
137
+ </select>
138
+ </SettingsField>
139
+
140
+ <SettingsField label="Size">
141
+ <select
142
+ value={bgSize}
143
+ onChange={(e) => updateBg({ background_size: e.target.value as CoverSection["background_size"] })}
144
+ className={SELECT_CLASS}
145
+ >
146
+ {BG_SIZE_OPTIONS.map((opt) => (
147
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
148
+ ))}
149
+ </select>
150
+ </SettingsField>
151
+ </SettingsSection>
152
+
153
+ {/* Overlay */}
154
+ <SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
155
+ <SettingsField label="Color">
156
+ <ColorSwatchPicker
157
+ value={overlayColor}
158
+ onChange={(val) => updateBg({ background_overlay_color: typeof val === "string" ? val : "" })}
159
+ swatches={paletteSwatches}
160
+ />
161
+ </SettingsField>
162
+
163
+ <SettingsField label="Opacity">
164
+ <div className="flex items-center gap-2">
165
+ <input
166
+ type="range"
167
+ min={0}
168
+ max={100}
169
+ step={5}
170
+ value={overlayOpacity}
171
+ onChange={(e) => updateBg({ background_overlay_opacity: parseInt(e.target.value) })}
172
+ className="flex-1 accent-[#076bff]"
173
+ />
174
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
175
+ {overlayOpacity}%
176
+ </span>
177
+ </div>
178
+ </SettingsField>
179
+ </SettingsSection>
180
+
181
+ {/* Section Height */}
182
+ <SettingsSection title="Height" defaultOpen icon={<SpacingIcon />}>
183
+ <SettingsField label="Height">
184
+ <select
185
+ value={section.height}
186
+ onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
187
+ className={SELECT_CLASS}
188
+ >
189
+ {HEIGHT_OPTIONS.map((opt) => (
190
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
191
+ ))}
192
+ </select>
193
+ </SettingsField>
194
+ </SettingsSection>
195
+
196
+ {/* Rows */}
197
+ <SettingsSection title="Rows" defaultOpen icon={<GridGapsIcon />}>
198
+ <div className="space-y-2">
199
+ {section.cover_rows.map((row, i) => (
200
+ <div
201
+ key={row._key}
202
+ className="flex items-center gap-2 rounded-lg bg-[#f5f5f5] px-2.5 py-2"
203
+ >
204
+ <span className="text-[10px] font-semibold text-neutral-400 w-6 shrink-0">
205
+ R{i + 1}
206
+ </span>
207
+ <span className="text-xs text-neutral-700 font-medium tabular-nums w-10">
208
+ {row.height_percent}%
209
+ </span>
210
+ <select
211
+ value={row.vertical_align}
212
+ onChange={(e) =>
213
+ store.updateCoverRowAlign(
214
+ section._key,
215
+ row._key,
216
+ e.target.value as "start" | "center" | "end"
217
+ )
218
+ }
219
+ className="flex-1 rounded-md border border-transparent bg-white px-2 py-1 text-[11px] text-neutral-700 outline-none hover:border-[#e5e5e5] focus:border-[#076bff]"
220
+ >
221
+ {ALIGN_OPTIONS.map((opt) => (
222
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
223
+ ))}
224
+ </select>
225
+ {section.cover_rows.length > 1 && (
226
+ <button
227
+ onClick={() => store.removeCoverRow(section._key, row._key)}
228
+ className="text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
229
+ title="Remove row"
230
+ >
231
+
232
+ </button>
233
+ )}
234
+ </div>
235
+ ))}
236
+ </div>
237
+
238
+ {section.cover_rows.length < 5 && (
239
+ <button
240
+ onClick={() => store.addCoverRow(section._key)}
241
+ className="w-full mt-2 rounded-lg border border-dashed border-neutral-300 py-2 text-[11px] font-medium text-neutral-400 hover:text-neutral-600 hover:border-neutral-400 transition-colors"
242
+ >
243
+ + Add Row
244
+ </button>
245
+ )}
246
+
247
+ <p className="text-[10px] text-neutral-400 leading-snug px-0.5 mt-2">
248
+ Drag the handles between rows in the canvas to resize. Heights always sum to 100%.
249
+ </p>
250
+ </SettingsSection>
251
+
252
+ {/* Grid Gaps */}
253
+ <SettingsSection title="Grid" defaultOpen={false} icon={<GridGapsIcon />}>
254
+ <SettingsField label="Col Gap">
255
+ <div className="flex items-center gap-2">
256
+ <input
257
+ type="range"
258
+ min={0}
259
+ max={60}
260
+ step={2}
261
+ value={section.settings.col_gap ?? 20}
262
+ onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
263
+ className="flex-1 accent-[#076bff]"
264
+ />
265
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
266
+ {section.settings.col_gap ?? 20}px
267
+ </span>
268
+ </div>
269
+ </SettingsField>
270
+
271
+ <SettingsField label="Row Gap">
272
+ <div className="flex items-center gap-2">
273
+ <input
274
+ type="range"
275
+ min={0}
276
+ max={60}
277
+ step={2}
278
+ value={section.settings.row_gap ?? 20}
279
+ onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
280
+ className="flex-1 accent-[#076bff]"
281
+ />
282
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
283
+ {section.settings.row_gap ?? 20}px
284
+ </span>
285
+ </div>
286
+ </SettingsField>
287
+ </SettingsSection>
288
+
289
+ {/* Stagger */}
290
+ <StaggerSettings
291
+ stagger={section.settings.stagger}
292
+ onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
293
+ />
294
+ </>
295
+ );
296
+ }
@@ -16,6 +16,7 @@ export { SectionV2AnimationTab } from "./SectionV2AnimationTab";
16
16
  export { default as ColumnV2Settings } from "./ColumnV2Settings";
17
17
  export { default as ParallaxSlideSettings } from "./ParallaxSlideSettings";
18
18
  export { default as ParallaxGroupSettings } from "./ParallaxGroupSettings";
19
+ export { default as CoverSectionSettings } from "./CoverSectionSettings";
19
20
  export { useSettingsPanelSelection } from "./useSettingsPanelSelection";
20
21
  export type { SelectedBlockInfo, SelectedParallaxSlideInfo } from "./useSettingsPanelSelection";
21
22
  export { AnimationTab, getBlockHoverEffect } from "./AnimationTab";
@@ -16,11 +16,13 @@ import type {
16
16
  ParallaxGroup,
17
17
  ParallaxSlideV2,
18
18
  SectionColumn,
19
+ CoverSection,
19
20
  } from "../../../lib/sanity/types";
20
21
  import {
21
22
  isPageSectionV2,
22
23
  isCustomSectionInstance,
23
24
  isParallaxGroup,
25
+ isCoverSection,
24
26
  } from "../../../lib/sanity/types";
25
27
 
26
28
  export interface SelectedBlockInfo {
@@ -43,6 +45,7 @@ export function useSettingsPanelSelection() {
43
45
  const selectedItem: ContentItem | undefined = store.rows.find((r) => r._key === store.selectedRowKey);
44
46
  const selectedSectionV2: PageSectionV2 | null = selectedItem && isPageSectionV2(selectedItem) ? selectedItem : null;
45
47
  const selectedCustomSectionInstance: CustomSectionInstance | null = selectedItem && isCustomSectionInstance(selectedItem) ? selectedItem as CustomSectionInstance : null;
48
+ const selectedCoverSection: CoverSection | null = selectedItem && isCoverSection(selectedItem) ? selectedItem as CoverSection : null;
46
49
 
47
50
  // Parallax detection: group selected directly, or slide selected (search inside groups)
48
51
  const selectedParallaxGroup: ParallaxGroup | null = selectedItem && isParallaxGroup(selectedItem) ? selectedItem as ParallaxGroup : null;
@@ -67,8 +70,22 @@ export function useSettingsPanelSelection() {
67
70
  return null;
68
71
  })();
69
72
 
70
- // V2 column: when a V2 section (or parallax slide) is selected and a column key is set
71
- const effectiveSectionV2 = selectedSectionV2 || selectedParallaxSlide?.virtualSection || null;
73
+ // Virtual section for cover sections (enables column/block selection routing)
74
+ const coverVirtualSection: PageSectionV2 | null = selectedCoverSection ? {
75
+ _type: "pageSectionV2",
76
+ _key: selectedCoverSection._key,
77
+ section_type: "empty-v2",
78
+ columns: selectedCoverSection.columns,
79
+ settings: {
80
+ preset: "custom",
81
+ grid_columns: selectedCoverSection.settings.grid_columns || 12,
82
+ col_gap: selectedCoverSection.settings.col_gap ?? 20,
83
+ row_gap: selectedCoverSection.settings.row_gap ?? 20,
84
+ },
85
+ } : null;
86
+
87
+ // V2 column: when a V2 section (or parallax slide or cover section) is selected and a column key is set
88
+ const effectiveSectionV2 = selectedSectionV2 || selectedParallaxSlide?.virtualSection || coverVirtualSection || null;
72
89
  const selectedColumnV2: SectionColumn | null = effectiveSectionV2 && store.selectedColumnKey
73
90
  ? effectiveSectionV2.columns.find((c) => c._key === store.selectedColumnKey) || null
74
91
  : null;
@@ -86,6 +103,15 @@ export function useSettingsPanelSelection() {
86
103
  if (block) return { block, rowKey: item._key, colKey: col._key, isSection: false };
87
104
  }
88
105
  }
106
+ // Cover sections: search inside columns
107
+ if (isCoverSection(item)) {
108
+ for (const col of (item as CoverSection).columns || []) {
109
+ const block = (col.blocks || []).find(
110
+ (b) => b._key === store.selectedBlockKey
111
+ );
112
+ if (block) return { block, rowKey: item._key, colKey: col._key, isSection: false };
113
+ }
114
+ }
89
115
  // Parallax groups: search inside slide columns
90
116
  if (isParallaxGroup(item)) {
91
117
  const group = item as ParallaxGroup;
@@ -118,6 +144,8 @@ export function useSettingsPanelSelection() {
118
144
  ? "Parallax Showcase"
119
145
  : selectedCustomSectionInstance
120
146
  ? (selectedCustomSectionInstance.custom_section_title || "Saved Section")
147
+ : selectedCoverSection
148
+ ? "Cover Section"
121
149
  : selectedSectionV2
122
150
  ? "Section"
123
151
  : "Page";
@@ -131,6 +159,8 @@ export function useSettingsPanelSelection() {
131
159
  ? "parallaxGroup"
132
160
  : selectedCustomSectionInstance
133
161
  ? "customSectionInstance"
162
+ : selectedCoverSection
163
+ ? "coverSection"
134
164
  : selectedSectionV2
135
165
  ? "row"
136
166
  : "page";
@@ -146,11 +176,14 @@ export function useSettingsPanelSelection() {
146
176
  const isCustomSectionOnly = !!(selectedCustomSectionInstance && !selectedBlock);
147
177
  // Page level: nothing selected — show Settings + SEO + Animation (no Layout)
148
178
  const isPageLevel = !hasSelection;
179
+ // Cover section: show all 3 tabs (Settings, Layout, Animation)
180
+ const isCoverSectionOnly = !!(selectedCoverSection && !selectedColumnV2 && !selectedBlock);
149
181
 
150
182
  return {
151
183
  selectedItem,
152
184
  selectedSectionV2,
153
185
  selectedCustomSectionInstance,
186
+ selectedCoverSection,
154
187
  selectedParallaxGroup,
155
188
  selectedParallaxSlide,
156
189
  effectiveSectionV2,
@@ -164,6 +197,7 @@ export function useSettingsPanelSelection() {
164
197
  isColumnOnly,
165
198
  isParallaxGroupOnly,
166
199
  isCustomSectionOnly,
200
+ isCoverSectionOnly,
167
201
  isPageLevel,
168
202
  };
169
203
  }
@@ -73,7 +73,6 @@ export const BLOCK_ENTER_PRESETS: Record<BlockType, readonly EnterPreset[]> = {
73
73
  imageGridBlock: ["fade", "scale", "slide-up"],
74
74
  videoBlock: ["fade", "slide-up"],
75
75
  buttonBlock: ["fade", "slide-up", "scale"],
76
- coverBlock: ["fade", "blur", "reveal", "scale"],
77
76
  spacerBlock: [], // invisible — no animation
78
77
  projectGridBlock: [], // uses card_entrance system
79
78
  };
@@ -66,7 +66,6 @@ export const BLOCK_HOVER_PRESETS: Record<BlockType, readonly HoverPreset[]> = {
66
66
  imageGridBlock: ["tilt-3d"],
67
67
  videoBlock: [], // video has play/pause interaction
68
68
  buttonBlock: ["scale-up", "lift", "border-glow"],
69
- coverBlock: ["scale-up", "lift", "ripple", "rgb-shift", "pixelate"],
70
69
  spacerBlock: [], // invisible
71
70
  projectGridBlock: ["scale-up", "lift"], // per-card effect
72
71
  };
@@ -1,4 +1,4 @@
1
- import type { ContentBlock, ParallaxSlideV2, SectionV2Settings } from "../../lib/sanity/types";
1
+ import type { ContentBlock, ParallaxSlideV2, SectionV2Settings, CoverSection, CoverSectionSettings, CoverRow } from "../../lib/sanity/types";
2
2
  import type { BlockType } from "./types";
3
3
  import { generateKey } from "./utils";
4
4
  import type { EnterAnimationConfig, TypewriterConfig } from "../../lib/animation/enter-types";
@@ -38,6 +38,48 @@ export function createDefaultParallaxSlide(): ParallaxSlideV2 {
38
38
  };
39
39
  }
40
40
 
41
+ // ============================================
42
+ // Cover section defaults (Session 176)
43
+ // ============================================
44
+
45
+ export const DEFAULT_COVER_SECTION_SETTINGS: CoverSectionSettings = {
46
+ grid_columns: 12,
47
+ col_gap: 20,
48
+ row_gap: 20,
49
+ spacing_top: "0",
50
+ spacing_bottom: "0",
51
+ };
52
+
53
+ export function createDefaultCoverRow(heightPercent: number = 100): CoverRow {
54
+ return {
55
+ _key: generateKey(),
56
+ height_percent: heightPercent,
57
+ vertical_align: "start",
58
+ };
59
+ }
60
+
61
+ export function createDefaultCoverSection(): CoverSection {
62
+ return {
63
+ _type: "coverSection",
64
+ _key: generateKey(),
65
+ background_type: "image",
66
+ background_position: "center center",
67
+ background_size: "cover",
68
+ background_overlay_color: "#000000",
69
+ background_overlay_opacity: 0,
70
+ height: "100vh",
71
+ cover_rows: [createDefaultCoverRow(100)],
72
+ columns: [{
73
+ _key: generateKey(),
74
+ grid_column: 1,
75
+ grid_row: 1,
76
+ span: 12,
77
+ blocks: [],
78
+ }],
79
+ settings: { ...DEFAULT_COVER_SECTION_SETTINGS },
80
+ };
81
+ }
82
+
41
83
  // ============================================
42
84
  // Default animation configs per block type (Session 117)
43
85
  // ============================================
@@ -138,27 +180,6 @@ export function createDefaultBlock(blockType: BlockType): ContentBlock {
138
180
  style: "primary",
139
181
  size: "medium",
140
182
  };
141
- case "coverBlock":
142
- return {
143
- _type: "coverBlock",
144
- _key,
145
- headline: "",
146
- subheadline: "",
147
- media_type: "image",
148
- media_path: "",
149
- background_size: "cover",
150
- background_position: "center center",
151
- background_repeat: "no-repeat",
152
- overlay: "dark",
153
- overlay_opacity: 50,
154
- content_align_h: "center",
155
- content_align_v: "center",
156
- content_max_width: "800px",
157
- height: "100vh",
158
- mobile_height: "same",
159
- text_color: "#ffffff",
160
- show_scroll_indicator: false,
161
- };
162
183
  case "projectGridBlock":
163
184
  return {
164
185
  _type: "projectGridBlock",
@@ -5,7 +5,7 @@
5
5
  * Extracted from serializer.ts in Session 162.
6
6
  */
7
7
 
8
- import type { Page, ContentBlock, ContentItem, PageSectionV2, SectionColumn, SectionV2Settings, CustomSectionInstance, ParallaxGroup } from "../../../lib/sanity/types";
8
+ import type { Page, ContentBlock, ContentItem, PageSectionV2, SectionColumn, SectionV2Settings, CustomSectionInstance, ParallaxGroup, CoverSection } from "../../../lib/sanity/types";
9
9
  import type { BuilderState, PageSettings } from "../types";
10
10
  import { generateKey } from "../utils";
11
11
  import { DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR, DEFAULT_GRID_WIDTH } from "../constants";
@@ -175,6 +175,39 @@ export function migrateContentItem(item: Record<string, unknown>): ContentItem {
175
175
  return normalizeParallaxGroup(item as unknown as Partial<ParallaxGroup> & { _key: string });
176
176
  }
177
177
 
178
+ // CoverSection — normalize with defaults for null fields (Session 176)
179
+ if (item._type === "coverSection") {
180
+ const raw = item as unknown as CoverSection;
181
+ const coverRows = (raw.cover_rows ?? []).map((r) => ({
182
+ ...r,
183
+ _key: r._key || generateKey(),
184
+ height_percent: r.height_percent ?? 100,
185
+ vertical_align: r.vertical_align ?? "start",
186
+ }));
187
+ return {
188
+ ...raw,
189
+ _key: (raw._key as string) || generateKey(),
190
+ background_type: raw.background_type ?? "image",
191
+ background_position: raw.background_position ?? "center center",
192
+ background_size: raw.background_size ?? "cover",
193
+ background_overlay_color: raw.background_overlay_color ?? "#000000",
194
+ background_overlay_opacity: raw.background_overlay_opacity ?? 0,
195
+ height: raw.height ?? "100vh",
196
+ cover_rows: coverRows.length > 0 ? coverRows : [{ _key: generateKey(), height_percent: 100, vertical_align: "start" as const }],
197
+ columns: (raw.columns ?? []).map((col) => ({
198
+ ...col,
199
+ _key: col._key || generateKey(),
200
+ blocks: ensureKeys(col.blocks ?? []) as ContentBlock[],
201
+ })),
202
+ settings: {
203
+ ...(raw.settings ?? {}),
204
+ grid_columns: (raw.settings?.grid_columns as number) ?? 12,
205
+ col_gap: (raw.settings?.col_gap as number) ?? 20,
206
+ row_gap: (raw.settings?.row_gap as number) ?? 20,
207
+ },
208
+ } as CoverSection;
209
+ }
210
+
178
211
  // Unknown type — warn and skip
179
212
  console.warn(`[Serializer] Unknown content item type: ${item._type}, skipping`);
180
213
  // Return a dummy PageSectionV2 as fallback
@@ -5,8 +5,8 @@
5
5
  * Extracted from serializer.ts in Session 162.
6
6
  */
7
7
 
8
- import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup } from "../../../lib/sanity/types";
9
- import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../lib/sanity/types";
8
+ import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup, CoverSection } from "../../../lib/sanity/types";
9
+ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSection } from "../../../lib/sanity/types";
10
10
  import type { BuilderState } from "../types";
11
11
  import { DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR } from "../constants";
12
12
 
@@ -210,6 +210,39 @@ function serializeParallaxGroup(group: ParallaxGroup): Record<string, unknown> {
210
210
  };
211
211
  }
212
212
 
213
+ // ============================================
214
+ // Cover Section Serialization (Session 176)
215
+ // ============================================
216
+
217
+ function serializeCoverSection(section: CoverSection): Record<string, unknown> {
218
+ const columns = (section.columns || []).map((col) => stripUndefined({
219
+ _type: "sectionColumn",
220
+ _key: col._key,
221
+ grid_column: col.grid_column,
222
+ grid_row: col.grid_row,
223
+ span: col.span,
224
+ blocks: (col.blocks || []).map((b) => serializeBlock(b)),
225
+ enter_animation: col.enter_animation,
226
+ } as Record<string, unknown>));
227
+
228
+ return stripUndefined({
229
+ _type: "coverSection",
230
+ _key: section._key,
231
+ background_type: section.background_type,
232
+ background_image: section.background_image,
233
+ background_video: section.background_video,
234
+ background_position: section.background_position,
235
+ background_size: section.background_size,
236
+ background_overlay_color: section.background_overlay_color,
237
+ background_overlay_opacity: section.background_overlay_opacity,
238
+ height: section.height,
239
+ cover_rows: section.cover_rows,
240
+ columns,
241
+ settings: stripUndefined(section.settings as unknown as Record<string, unknown>),
242
+ responsive: section.responsive,
243
+ } as Record<string, unknown>);
244
+ }
245
+
213
246
  // ============================================
214
247
  // Content Item Dispatcher
215
248
  // ============================================
@@ -248,6 +281,10 @@ function serializeContentItem(item: ContentItem): Record<string, unknown> {
248
281
  if (isParallaxGroup(item)) {
249
282
  return serializeParallaxGroup(item);
250
283
  }
284
+ // CoverSection — serialize background, rows, columns, settings (Session 176)
285
+ if (isCoverSection(item)) {
286
+ return serializeCoverSection(item as CoverSection);
287
+ }
251
288
  // Should never reach here
252
289
  throw new Error(`[Serializer] Unknown content item type during serialization`);
253
290
  }
@@ -1,6 +1,6 @@
1
1
  import type { BuilderStore, BuilderState } from "./types";
2
- import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup } from "../../lib/sanity/types";
3
- import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
2
+ import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup, CoverSection } from "../../lib/sanity/types";
3
+ import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
4
4
  import { createDefaultBlock } from "./defaults";
5
5
  import { generateKey } from "./utils";
6
6
  import { findSectionPath, updateSectionAtPath, moveBlockInState } from "./store-helpers";
@@ -30,6 +30,9 @@ function applyBlockUpdate(
30
30
  if (isPageSectionV2(item)) {
31
31
  return { ...item, columns: updateColumns(item.columns) };
32
32
  }
33
+ if (isCoverSection(item)) {
34
+ return { ...item, columns: updateColumns((item as CoverSection).columns) } as ContentItem;
35
+ }
33
36
  if (isParallaxGroup(item)) {
34
37
  return {
35
38
  ...item,
@@ -61,11 +64,13 @@ export function createBlockActions(set: StoreSet, get: StoreGet) {
61
64
  cols.map((c) => ({ ...c, blocks: c.blocks.filter((b) => b._key !== blockKey) }));
62
65
 
63
66
  set((state) => {
64
- // Handle V2 Sections + ParallaxGroup slides
65
67
  const finalRows = state.rows.map((item) => {
66
68
  if (isPageSectionV2(item)) {
67
69
  return { ...item, columns: filterBlocks(item.columns) } as ContentItem;
68
70
  }
71
+ if (isCoverSection(item)) {
72
+ return { ...item, columns: filterBlocks((item as CoverSection).columns) } as ContentItem;
73
+ }
69
74
  if (isParallaxGroup(item)) {
70
75
  return {
71
76
  ...item,
@@ -105,6 +110,9 @@ export function createBlockActions(set: StoreSet, get: StoreGet) {
105
110
  if (isPageSectionV2(item)) {
106
111
  return { ...item, columns: dupInColumns(item.columns) } as ContentItem;
107
112
  }
113
+ if (isCoverSection(item)) {
114
+ return { ...item, columns: dupInColumns((item as CoverSection).columns) } as ContentItem;
115
+ }
108
116
  if (isParallaxGroup(item)) {
109
117
  return {
110
118
  ...item,