@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,220 @@
1
+ /**
2
+ * Cover Section store actions.
3
+ *
4
+ * Manages cover-section-specific operations: row CRUD, row resize,
5
+ * background/settings updates. Column and block operations within
6
+ * cover sections are handled by the existing V2 actions via
7
+ * findSectionPath() which now supports cover sections.
8
+ *
9
+ * Session 176: Cover Sections — Phase 3 (Store).
10
+ */
11
+
12
+ import type { BuilderState } from "./types";
13
+ import type {
14
+ ContentItem,
15
+ CoverSection,
16
+ CoverSectionSettings,
17
+ CoverRow,
18
+ } from "../../lib/sanity/types";
19
+ import { isCoverSection } from "../../lib/sanity/types";
20
+ import { generateKey } from "./utils";
21
+ import { createDefaultCoverSection, createDefaultCoverRow } from "./defaults";
22
+
23
+ type StoreSet = (
24
+ partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
25
+ ) => void;
26
+ type StoreGet = () => BuilderState & { _pushSnapshot: () => void };
27
+
28
+ const MIN_ROW_PERCENT = 10;
29
+
30
+ function updateCoverInRows(
31
+ rows: ContentItem[],
32
+ sectionKey: string,
33
+ updater: (section: CoverSection) => CoverSection
34
+ ): ContentItem[] {
35
+ return rows.map((item) => {
36
+ if (item._key === sectionKey && isCoverSection(item)) {
37
+ return updater(item as CoverSection) as ContentItem;
38
+ }
39
+ return item;
40
+ });
41
+ }
42
+
43
+ export function createCoverActions(set: StoreSet, get: StoreGet) {
44
+ return {
45
+ addCoverSection: (afterRowKey?: string | null): void => {
46
+ get()._pushSnapshot();
47
+ const section = createDefaultCoverSection();
48
+ set((state) => {
49
+ const rows = [...state.rows];
50
+ if (afterRowKey) {
51
+ const idx = rows.findIndex((r) => r._key === afterRowKey);
52
+ if (idx !== -1) {
53
+ rows.splice(idx + 1, 0, section);
54
+ } else {
55
+ rows.push(section);
56
+ }
57
+ } else {
58
+ rows.push(section);
59
+ }
60
+ return { rows, isDirty: true, selectedRowKey: section._key };
61
+ });
62
+ },
63
+
64
+ addCoverRow: (sectionKey: string): void => {
65
+ get()._pushSnapshot();
66
+ set((state) => ({
67
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => {
68
+ if (section.cover_rows.length >= 5) return section;
69
+ const totalRows = section.cover_rows.length + 1;
70
+ const equalPercent = Math.floor(100 / totalRows);
71
+ const remainder = 100 - equalPercent * totalRows;
72
+ const newRows = section.cover_rows.map((r, i) => ({
73
+ ...r,
74
+ height_percent: equalPercent + (i === 0 ? remainder : 0),
75
+ }));
76
+ newRows.push(createDefaultCoverRow(equalPercent));
77
+ return { ...section, cover_rows: newRows };
78
+ }),
79
+ isDirty: true,
80
+ }));
81
+ },
82
+
83
+ removeCoverRow: (sectionKey: string, rowKey: string): void => {
84
+ get()._pushSnapshot();
85
+ set((state) => ({
86
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => {
87
+ if (section.cover_rows.length <= 1) return section;
88
+ const rowIndex = section.cover_rows.findIndex((r) => r._key === rowKey);
89
+ if (rowIndex === -1) return section;
90
+ const removedPercent = section.cover_rows[rowIndex].height_percent;
91
+ const remaining = section.cover_rows.filter((r) => r._key !== rowKey);
92
+
93
+ const neighborIndex = rowIndex > 0 ? rowIndex - 1 : 0;
94
+ const redistributed = remaining.map((r, i) => {
95
+ if (remaining.length === 1) {
96
+ return { ...r, height_percent: 100 };
97
+ }
98
+ if (i === neighborIndex) {
99
+ return { ...r, height_percent: r.height_percent + removedPercent };
100
+ }
101
+ return r;
102
+ });
103
+
104
+ const filteredColumns = section.columns.filter(
105
+ (c) => c.grid_row !== rowIndex + 1
106
+ );
107
+ const reindexedColumns = filteredColumns.map((c) => ({
108
+ ...c,
109
+ grid_row: c.grid_row > rowIndex + 1 ? c.grid_row - 1 : c.grid_row,
110
+ }));
111
+
112
+ return {
113
+ ...section,
114
+ cover_rows: redistributed,
115
+ columns: reindexedColumns,
116
+ };
117
+ }),
118
+ isDirty: true,
119
+ }));
120
+ },
121
+
122
+ /**
123
+ * Resize cover rows by dragging the handle between row[handleIndex] and row[handleIndex+1].
124
+ * deltaPercent is the total delta from the start of the drag.
125
+ * startAbove/startBelow are the original percentages captured at mousedown —
126
+ * computation is always from these start values to avoid compounding.
127
+ */
128
+ resizeCoverRow: (sectionKey: string, handleIndex: number, deltaPercent: number, startAbove: number, startBelow: number): void => {
129
+ set((state) => ({
130
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => {
131
+ const rows = section.cover_rows;
132
+ if (handleIndex < 0 || handleIndex >= rows.length - 1) return section;
133
+
134
+ const total = startAbove + startBelow;
135
+ let newAbove = startAbove + deltaPercent;
136
+ let newBelow = startBelow - deltaPercent;
137
+
138
+ if (newAbove < MIN_ROW_PERCENT) {
139
+ newAbove = MIN_ROW_PERCENT;
140
+ newBelow = total - MIN_ROW_PERCENT;
141
+ }
142
+ if (newBelow < MIN_ROW_PERCENT) {
143
+ newBelow = MIN_ROW_PERCENT;
144
+ newAbove = total - MIN_ROW_PERCENT;
145
+ }
146
+
147
+ const updatedRows = rows.map((r, i) => {
148
+ if (i === handleIndex) return { ...r, height_percent: newAbove };
149
+ if (i === handleIndex + 1) return { ...r, height_percent: newBelow };
150
+ return r;
151
+ });
152
+
153
+ return { ...section, cover_rows: updatedRows };
154
+ }),
155
+ isDirty: true,
156
+ }));
157
+ },
158
+
159
+ updateCoverRowAlign: (
160
+ sectionKey: string,
161
+ rowKey: string,
162
+ align: CoverRow["vertical_align"]
163
+ ): void => {
164
+ set((state) => ({
165
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
166
+ ...section,
167
+ cover_rows: section.cover_rows.map((r) =>
168
+ r._key === rowKey ? { ...r, vertical_align: align } : r
169
+ ),
170
+ })),
171
+ isDirty: true,
172
+ }));
173
+ },
174
+
175
+ updateCoverBackground: (
176
+ sectionKey: string,
177
+ fields: Partial<Pick<CoverSection,
178
+ "background_type" | "background_image" | "background_video" |
179
+ "background_position" | "background_size" |
180
+ "background_overlay_color" | "background_overlay_opacity"
181
+ >>
182
+ ): void => {
183
+ get()._pushSnapshot();
184
+ set((state) => ({
185
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
186
+ ...section,
187
+ ...fields,
188
+ })),
189
+ isDirty: true,
190
+ }));
191
+ },
192
+
193
+ updateCoverSettings: (
194
+ sectionKey: string,
195
+ settings: Partial<CoverSectionSettings>
196
+ ): void => {
197
+ set((state) => ({
198
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
199
+ ...section,
200
+ settings: { ...section.settings, ...settings },
201
+ })),
202
+ isDirty: true,
203
+ }));
204
+ },
205
+
206
+ updateCoverHeight: (
207
+ sectionKey: string,
208
+ height: CoverSection["height"]
209
+ ): void => {
210
+ get()._pushSnapshot();
211
+ set((state) => ({
212
+ rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
213
+ ...section,
214
+ height,
215
+ })),
216
+ isDirty: true,
217
+ }));
218
+ },
219
+ };
220
+ }
@@ -15,8 +15,9 @@ import type {
15
15
  ParallaxSlideV2,
16
16
  SectionColumn,
17
17
  SectionV2Preset,
18
+ CoverSection,
18
19
  } from "../../lib/sanity/types";
19
- import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
20
+ import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
20
21
  import { columnsFromPreset, detectPreset } from "./cascade";
21
22
  import { resizeColumnLeft as cascadeResizeLeft, moveColumn as cascadeMoveColumn, type ResizeLeftResult } from "./cascade";
22
23
  import { applyBlocksToColumns, toCascadeColumns, type CascadeColumn } from "./cascade-helpers";
@@ -74,6 +75,14 @@ export function moveBlockInState(
74
75
  );
75
76
  break;
76
77
  }
78
+ } else if (isCoverSection(item)) {
79
+ const updatedCols = removeFromColumns((item as CoverSection).columns);
80
+ if (movedBlock) {
81
+ rowsAfterRemove = rows.map((r, idx) =>
82
+ idx === i ? { ...r, columns: updatedCols } as ContentItem : r
83
+ );
84
+ break;
85
+ }
77
86
  } else if (isParallaxGroup(item)) {
78
87
  const group = item as ParallaxGroup;
79
88
  let foundInSlide = false;
@@ -217,8 +226,8 @@ export function addSectionV2InState(
217
226
  grid_columns: gridColumns,
218
227
  col_gap: 20,
219
228
  row_gap: 20,
220
- spacing_top: "32",
221
- spacing_bottom: "32",
229
+ spacing_top: "0",
230
+ spacing_bottom: "0",
222
231
  },
223
232
  };
224
233
 
@@ -369,7 +378,8 @@ export function moveColumnToGapV2InState(
369
378
 
370
379
  export type SectionPath =
371
380
  | { type: "v2"; index: number }
372
- | { type: "parallaxSlide"; groupIndex: number; slideIndex: number };
381
+ | { type: "parallaxSlide"; groupIndex: number; slideIndex: number }
382
+ | { type: "cover"; index: number };
373
383
 
374
384
  /**
375
385
  * Find a V2 section by key — searches both top-level PageSectionV2
@@ -388,6 +398,9 @@ export function findSectionPath(
388
398
  if (item._key === sectionKey && isPageSectionV2(item)) {
389
399
  return { type: "v2", index: i };
390
400
  }
401
+ if (item._key === sectionKey && isCoverSection(item)) {
402
+ return { type: "cover", index: i };
403
+ }
391
404
  if (isParallaxGroup(item)) {
392
405
  const group = item as ParallaxGroup;
393
406
  for (let j = 0; j < group.slides.length; j++) {
@@ -412,6 +425,29 @@ export function getSectionFromPath(
412
425
  const item = rows[path.index];
413
426
  return isPageSectionV2(item) ? (item as PageSectionV2) : null;
414
427
  }
428
+ if (path.type === "cover") {
429
+ const item = rows[path.index];
430
+ if (!isCoverSection(item)) return null;
431
+ const cover = item as CoverSection;
432
+ return {
433
+ _type: "pageSectionV2",
434
+ _key: cover._key,
435
+ section_type: "empty-v2",
436
+ columns: cover.columns,
437
+ settings: {
438
+ preset: "custom",
439
+ grid_columns: cover.settings.grid_columns,
440
+ col_gap: cover.settings.col_gap,
441
+ row_gap: cover.settings.row_gap,
442
+ spacing_top: cover.settings.spacing_top,
443
+ spacing_right: cover.settings.spacing_right,
444
+ spacing_bottom: cover.settings.spacing_bottom,
445
+ spacing_left: cover.settings.spacing_left,
446
+ enter_animation: cover.settings.enter_animation,
447
+ stagger: cover.settings.stagger,
448
+ },
449
+ };
450
+ }
415
451
  const group = rows[path.groupIndex];
416
452
  if (!isParallaxGroup(group)) return null;
417
453
  const slide = (group as ParallaxGroup).slides[path.slideIndex];
@@ -440,6 +476,47 @@ export function updateSectionAtPath(
440
476
  return updater(item as PageSectionV2) as ContentItem;
441
477
  });
442
478
  }
479
+ if (path.type === "cover") {
480
+ return rows.map((item, i) => {
481
+ if (i !== path.index || !isCoverSection(item)) return item;
482
+ const cover = item as CoverSection;
483
+ const virtualSection: PageSectionV2 = {
484
+ _type: "pageSectionV2",
485
+ _key: cover._key,
486
+ section_type: "empty-v2",
487
+ columns: cover.columns,
488
+ settings: {
489
+ preset: "custom",
490
+ grid_columns: cover.settings.grid_columns,
491
+ col_gap: cover.settings.col_gap,
492
+ row_gap: cover.settings.row_gap,
493
+ spacing_top: cover.settings.spacing_top,
494
+ spacing_right: cover.settings.spacing_right,
495
+ spacing_bottom: cover.settings.spacing_bottom,
496
+ spacing_left: cover.settings.spacing_left,
497
+ enter_animation: cover.settings.enter_animation,
498
+ stagger: cover.settings.stagger,
499
+ },
500
+ };
501
+ const updated = updater(virtualSection);
502
+ return {
503
+ ...cover,
504
+ columns: updated.columns,
505
+ settings: {
506
+ ...cover.settings,
507
+ grid_columns: updated.settings.grid_columns,
508
+ col_gap: updated.settings.col_gap,
509
+ row_gap: updated.settings.row_gap,
510
+ spacing_top: updated.settings.spacing_top,
511
+ spacing_right: updated.settings.spacing_right,
512
+ spacing_bottom: updated.settings.spacing_bottom,
513
+ spacing_left: updated.settings.spacing_left,
514
+ enter_animation: updated.settings.enter_animation,
515
+ stagger: updated.settings.stagger,
516
+ },
517
+ } as ContentItem;
518
+ });
519
+ }
443
520
  // parallaxSlide path
444
521
  return rows.map((item, i) => {
445
522
  if (i !== path.groupIndex || !isParallaxGroup(item)) return item;
@@ -7,6 +7,7 @@ import type {
7
7
  CustomSectionInstance,
8
8
  ParallaxGroup,
9
9
  ParallaxSlideV2,
10
+ CoverSection,
10
11
  SectionV2Preset,
11
12
  EnterAnimationConfig,
12
13
  } from "../../lib/sanity/types";
@@ -14,6 +15,7 @@ import {
14
15
  isPageSectionV2,
15
16
  isCustomSectionInstance,
16
17
  isParallaxGroup,
18
+ isCoverSection,
17
19
  } from "../../lib/sanity/types";
18
20
  import { createDefaultBlock, createDefaultParallaxSlide } from "./defaults";
19
21
  import { generateKey } from "./utils";
@@ -78,8 +80,8 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
78
80
  grid_columns: gridColumns,
79
81
  col_gap: 20,
80
82
  row_gap: 20,
81
- spacing_top: "32",
82
- spacing_bottom: "32",
83
+ spacing_top: "0",
84
+ spacing_bottom: "0",
83
85
  },
84
86
  };
85
87
 
@@ -141,6 +143,14 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
141
143
  blocks: col.blocks.map((b) => ({ ...b, _key: generateKey() })),
142
144
  })
143
145
  );
146
+ } else if (isCoverSection(clone)) {
147
+ const cover = clone as CoverSection;
148
+ cover.cover_rows = cover.cover_rows.map((r) => ({ ...r, _key: generateKey() }));
149
+ cover.columns = cover.columns.map((col) => ({
150
+ ...col,
151
+ _key: generateKey(),
152
+ blocks: col.blocks.map((b) => ({ ...b, _key: generateKey() })),
153
+ }));
144
154
  } else if (isParallaxGroup(clone)) {
145
155
  const group = clone as ParallaxGroup;
146
156
  group.slides = group.slides.map((slide) => ({
@@ -3,8 +3,8 @@ import type { BuilderStore, BuilderState, BlockType, PageSettings, CanvasTool, D
3
3
  import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
4
4
  import { stateToDocument, documentToState } from "./serializer";
5
5
  import { generateKey } from "./utils";
6
- import type { ContentBlock, ContentItem, PageSectionV2, SectionV2Settings, PageMetadata, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
7
- import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
6
+ import type { ContentBlock, ContentItem, PageSectionV2, SectionV2Settings, PageMetadata, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2, CoverSection } from "../../lib/sanity/types";
7
+ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
8
8
  import { createDefaultBlock, createDefaultParallaxSlide } from "./defaults";
9
9
  import { MAX_HISTORY, pushSnapshot } from "./history";
10
10
  import {
@@ -33,6 +33,7 @@ import { revalidateSite } from "../../lib/revalidate";
33
33
  import { createSectionActions } from "./store-sections";
34
34
  import { createBlockActions } from "./store-blocks";
35
35
  import { createCanvasActions } from "./store-canvas";
36
+ import { createCoverActions } from "./store-cover";
36
37
 
37
38
  // ============================================
38
39
  // RC-003 fix: Block parent key cache — O(1) lookup in selectBlock().
@@ -55,6 +56,13 @@ function getBlockParentCache(rows: ContentItem[]): Map<string, BlockParent> {
55
56
  cache.set(b._key, { rowKey: item._key, colKey: col._key });
56
57
  }
57
58
  }
59
+ } else if (isCoverSection(item)) {
60
+ const cover = item as CoverSection;
61
+ for (const col of cover.columns) {
62
+ for (const b of col.blocks) {
63
+ cache.set(b._key, { rowKey: item._key, colKey: col._key });
64
+ }
65
+ }
58
66
  } else if (isParallaxGroup(item)) {
59
67
  const group = item as ParallaxGroup;
60
68
  for (const slide of group.slides) {
@@ -346,4 +354,5 @@ export const useBuilderStore = create<BuilderStore>((set, get) => ({
346
354
  ...createSectionActions(set, get),
347
355
  ...createBlockActions(set, get),
348
356
  ...createCanvasActions(set, get),
357
+ ...createCoverActions(set, get),
349
358
  }));
@@ -15,6 +15,9 @@ import type {
15
15
  SectionColumn,
16
16
  PageMetadata,
17
17
  ColorField,
18
+ CoverSection,
19
+ CoverSectionSettings,
20
+ CoverRow,
18
21
  } from "../../lib/sanity/types";
19
22
  import { DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR, DEFAULT_GRID_WIDTH } from "./constants";
20
23
 
@@ -48,7 +51,6 @@ export const BLOCK_TYPE_REGISTRY: BlockTypeInfo[] = [
48
51
  { type: "videoBlock", label: "Video", description: "Vimeo, YouTube, or MP4", group: "generic", icon: "▶", category: "content" },
49
52
  { type: "spacerBlock", label: "Spacer", description: "Vertical spacing", group: "generic", icon: "↕", category: "content" },
50
53
  { type: "buttonBlock", label: "Button", description: "Call-to-action button", group: "generic", icon: "▣", category: "content" },
51
- { type: "coverBlock", label: "Cover", description: "Full-screen hero section", group: "generic", icon: "◈", category: "content" },
52
54
  ];
53
55
 
54
56
  /**
@@ -90,7 +92,7 @@ export function isSectionBlockSection(section: { columns?: Array<{ blocks?: Arra
90
92
  return blocks.length === 1 && SECTION_BLOCK_TYPES.has(blocks[0]._type);
91
93
  }
92
94
 
93
- export type SectionType = "empty-v2" | "parallaxGroup" | SectionBlockType;
95
+ export type SectionType = "empty-v2" | "parallaxGroup" | "coverSection" | SectionBlockType;
94
96
 
95
97
  export interface SectionTypeInfo {
96
98
  type: SectionType;
@@ -103,6 +105,7 @@ export interface SectionTypeInfo {
103
105
 
104
106
  export const SECTION_TYPE_REGISTRY: SectionTypeInfo[] = [
105
107
  { type: "empty-v2", label: "Empty Section", description: "Grid section with flexible columns", icon: "⊞" },
108
+ { type: "coverSection", label: "Cover Section", description: "Full-viewport section with background and proportional rows", icon: "◆" },
106
109
  { type: "projectGridBlock", label: "Project Grid", description: "Staggered project showcase grid", icon: "⬡", blockType: "projectGridBlock" },
107
110
  { type: "parallaxGroup", label: "Parallax Section", description: "Full-screen parallax showcase with V2 slides", icon: "▽" },
108
111
  ];
@@ -374,6 +377,16 @@ export interface BuilderActions {
374
377
  /** Update group-level settings (transition effect, snap, etc.) */
375
378
  updateParallaxGroupSettings: (groupKey: string, fields: Partial<Pick<import("../../lib/sanity/types").ParallaxGroup, "transition_effect" | "snap_enabled" | "parallax_intensity">>) => void;
376
379
 
380
+ // ---- Cover Section operations (Session 176) ----
381
+ addCoverSection: (afterRowKey?: string | null) => void;
382
+ addCoverRow: (sectionKey: string) => void;
383
+ removeCoverRow: (sectionKey: string, rowKey: string) => void;
384
+ resizeCoverRow: (sectionKey: string, handleIndex: number, deltaPercent: number, startAbove: number, startBelow: number) => void;
385
+ updateCoverRowAlign: (sectionKey: string, rowKey: string, align: CoverRow["vertical_align"]) => void;
386
+ updateCoverBackground: (sectionKey: string, fields: Partial<Pick<CoverSection, "background_type" | "background_image" | "background_video" | "background_position" | "background_size" | "background_overlay_color" | "background_overlay_opacity">>) => void;
387
+ updateCoverSettings: (sectionKey: string, settings: Partial<CoverSectionSettings>) => void;
388
+ updateCoverHeight: (sectionKey: string, height: CoverSection["height"]) => void;
389
+
377
390
  // Page-level settings
378
391
  updatePageSettings: (settings: Partial<PageSettings>) => void;
379
392
  applyGlobalStyles: () => Promise<void>;
@@ -5,14 +5,14 @@ import { groq } from "next-sanity";
5
5
  // ============================================
6
6
 
7
7
  // Deep expansion of content_rows.
8
- // Handles PageSectionV2, CustomSectionInstance, and ParallaxGroup.
8
+ // Handles PageSectionV2, CustomSectionInstance, ParallaxGroup, and CoverSection.
9
9
  // GROQ projections are additive — fields that don't exist on an object are omitted.
10
10
  const blockExpansion = `
11
11
  content_rows[] {
12
12
  _key,
13
13
  _type,
14
14
  section_type,
15
- // ── V2 section columns ──
15
+ // ── V2 section columns (shared by PageSectionV2 and CoverSection) ──
16
16
  columns[] {
17
17
  _key,
18
18
  _type,
@@ -73,9 +73,23 @@ const blockExpansion = `
73
73
  }
74
74
  }
75
75
  },
76
- // ── Settings (V2 section settings) ──
76
+ // ── CoverSection fields (only present when _type == "coverSection") ──
77
+ background_type,
78
+ background_image,
79
+ background_video,
80
+ background_position,
81
+ background_size,
82
+ background_overlay_color,
83
+ background_overlay_opacity,
84
+ height,
85
+ cover_rows[] {
86
+ _key,
87
+ height_percent,
88
+ vertical_align
89
+ },
90
+ // ── Settings (V2 section settings + CoverSection settings) ──
77
91
  settings,
78
- // ── Responsive overrides (V2 sections store responsive at section level) ──
92
+ // ── Responsive overrides (V2 + CoverSection store responsive at section level) ──
79
93
  responsive
80
94
  }
81
95
  `;