@morphika/andami 0.2.12 → 0.2.13
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/PageRenderer.tsx +13 -9
- 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/index.ts +0 -1
- package/components/builder/index.ts +1 -0
- 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/types.ts +79 -43
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/index.ts +1 -2
- 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
|
@@ -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
|
-
//
|
|
71
|
-
const
|
|
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
|
};
|
package/lib/builder/defaults.ts
CHANGED
|
@@ -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,
|
|
@@ -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
|
+
}
|