@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
|
@@ -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: "
|
|
221
|
-
spacing_bottom: "
|
|
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: "
|
|
82
|
-
spacing_bottom: "
|
|
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) => ({
|
package/lib/builder/store.ts
CHANGED
|
@@ -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
|
}));
|
package/lib/builder/types.ts
CHANGED
|
@@ -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>;
|
package/lib/sanity/queries.ts
CHANGED
|
@@ -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
|
|
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
|
-
// ──
|
|
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
|
|
92
|
+
// ── Responsive overrides (V2 + CoverSection store responsive at section level) ──
|
|
79
93
|
responsive
|
|
80
94
|
}
|
|
81
95
|
`;
|