@morphika/andami 0.5.0 → 0.5.2
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 +151 -36
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -327
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -227
- package/components/builder/BlockTypePicker.tsx +3 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -275
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +20 -4
- package/components/builder/SectionV2Column.tsx +74 -68
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +192 -173
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +7 -7
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +3 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +3 -0
- package/lib/builder/block-registrations.ts +468 -335
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +6 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +156 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +12 -9
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -111
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -1,275 +1,363 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
4
|
-
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
|
-
import type { CoverSection, CoverRow, PageSectionV2 } from "../../lib/sanity/types";
|
|
6
|
-
import type { DeviceViewport } from "../../lib/builder/types";
|
|
7
|
-
import SectionV2Canvas from "./SectionV2Canvas";
|
|
8
|
-
import CoverRowResizeHandle from "./CoverRowResizeHandle";
|
|
9
|
-
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
10
|
-
import { adminAssetUrl } from "../../lib/assets";
|
|
11
|
-
import { normalizeRowHeights } from "../../lib/builder/store-cover";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* CoverSectionCanvas — renders a CoverSection in the builder canvas.
|
|
15
|
-
*
|
|
16
|
-
* Displays proportional rows (based on cover_rows height_percent) inside
|
|
17
|
-
* a fixed-height container that simulates the section's vh height.
|
|
18
|
-
* Each row contains a SectionV2Canvas (full V2 grid editor reuse) via
|
|
19
|
-
* a virtual PageSectionV2 scoped to that row's columns.
|
|
20
|
-
*
|
|
21
|
-
* Background image/video is shown as a faint preview behind all rows.
|
|
22
|
-
* Row resize handles are rendered between adjacent rows (Phase 5).
|
|
23
|
-
*
|
|
24
|
-
* Session 176: Cover Sections — Phase 4 (Builder Canvas).
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
interface CoverSectionCanvasProps {
|
|
28
|
-
section: CoverSection;
|
|
29
|
-
onAddBlockTarget: (sectionKey: string, colKey: string, insertIndex?: number) => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const COVER_ACCENT = "#0d9488";
|
|
33
|
-
|
|
34
|
-
function getEffectiveCoverRows(section: CoverSection, viewport: DeviceViewport): CoverRow[] {
|
|
35
|
-
if (viewport === "desktop") return section.cover_rows;
|
|
36
|
-
const vp = viewport as "tablet" | "phone";
|
|
37
|
-
const overrides = section.responsive?.[vp]?.cover_rows;
|
|
38
|
-
if (!overrides || overrides.length === 0) return section.cover_rows;
|
|
39
|
-
|
|
40
|
-
// Partial overrides (only some rows have a tablet/phone value) produce a
|
|
41
|
-
// merged array whose sum is not necessarily 100 — desktop values for the
|
|
42
|
-
// non-overridden rows don't know about the tablet overrides. Normalize the
|
|
43
|
-
// final set so the CSS `grid-template-rows` stays valid.
|
|
44
|
-
const merged = section.cover_rows.map((row) => {
|
|
45
|
-
const override = overrides.find((o) => o._key === row._key);
|
|
46
|
-
if (override?.height_percent !== undefined) {
|
|
47
|
-
return { ...row, height_percent: override.height_percent };
|
|
48
|
-
}
|
|
49
|
-
return row;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const total = merged.reduce((acc, r) => acc + r.height_percent, 0);
|
|
53
|
-
if (Math.abs(total - 100) <= 0.5) return merged;
|
|
54
|
-
|
|
55
|
-
const normalized = normalizeRowHeights(merged.map((r) => r.height_percent));
|
|
56
|
-
return merged.map((r, i) => ({ ...r, height_percent: normalized[i] ?? r.height_percent }));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export default function CoverSectionCanvas({
|
|
60
|
-
section,
|
|
61
|
-
onAddBlockTarget,
|
|
62
|
-
}: CoverSectionCanvasProps) {
|
|
63
|
-
const store = useBuilderStore();
|
|
64
|
-
const activeViewport = store.activeViewport || "desktop";
|
|
65
|
-
const previewMode = store.previewMode;
|
|
66
|
-
const [isSectionHovered, setIsSectionHovered] = useState(false);
|
|
67
|
-
|
|
68
|
-
const vhPixels = DEVICE_HEIGHTS[activeViewport];
|
|
69
|
-
const heightMultiplier = (parseInt(section.height || "100", 10) || 100) / 100;
|
|
70
|
-
const containerHeight = Math.round(vhPixels * heightMultiplier);
|
|
71
|
-
|
|
72
|
-
const bgImageUrl = section.background_type === "image" && section.background_image
|
|
73
|
-
? adminAssetUrl(section.background_image)
|
|
74
|
-
: null;
|
|
75
|
-
const bgVideoUrl = section.background_type === "video" && section.background_video
|
|
76
|
-
? adminAssetUrl(section.background_video)
|
|
77
|
-
: null;
|
|
78
|
-
|
|
79
|
-
const effectiveRows = useMemo(
|
|
80
|
-
() => getEffectiveCoverRows(section, activeViewport),
|
|
81
|
-
[section, activeViewport]
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const virtualSectionsPerRow = useMemo(() => {
|
|
85
|
-
return effectiveRows.map((row, rowIndex) => {
|
|
86
|
-
const rowNumber = rowIndex + 1;
|
|
87
|
-
const
|
|
88
|
-
.filter((c) => c.grid_row === rowNumber)
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
|
+
import type { CoverSection, CoverRow, PageSectionV2, ColumnOverride } from "../../lib/sanity/types";
|
|
6
|
+
import type { DeviceViewport } from "../../lib/builder/types";
|
|
7
|
+
import SectionV2Canvas from "./SectionV2Canvas";
|
|
8
|
+
import CoverRowResizeHandle from "./CoverRowResizeHandle";
|
|
9
|
+
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
10
|
+
import { adminAssetUrl } from "../../lib/assets";
|
|
11
|
+
import { normalizeRowHeights } from "../../lib/builder/store-cover";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CoverSectionCanvas — renders a CoverSection in the builder canvas.
|
|
15
|
+
*
|
|
16
|
+
* Displays proportional rows (based on cover_rows height_percent) inside
|
|
17
|
+
* a fixed-height container that simulates the section's vh height.
|
|
18
|
+
* Each row contains a SectionV2Canvas (full V2 grid editor reuse) via
|
|
19
|
+
* a virtual PageSectionV2 scoped to that row's columns.
|
|
20
|
+
*
|
|
21
|
+
* Background image/video is shown as a faint preview behind all rows.
|
|
22
|
+
* Row resize handles are rendered between adjacent rows (Phase 5).
|
|
23
|
+
*
|
|
24
|
+
* Session 176: Cover Sections — Phase 4 (Builder Canvas).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface CoverSectionCanvasProps {
|
|
28
|
+
section: CoverSection;
|
|
29
|
+
onAddBlockTarget: (sectionKey: string, colKey: string, insertIndex?: number) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const COVER_ACCENT = "#0d9488";
|
|
33
|
+
|
|
34
|
+
function getEffectiveCoverRows(section: CoverSection, viewport: DeviceViewport): CoverRow[] {
|
|
35
|
+
if (viewport === "desktop") return section.cover_rows;
|
|
36
|
+
const vp = viewport as "tablet" | "phone";
|
|
37
|
+
const overrides = section.responsive?.[vp]?.cover_rows;
|
|
38
|
+
if (!overrides || overrides.length === 0) return section.cover_rows;
|
|
39
|
+
|
|
40
|
+
// Partial overrides (only some rows have a tablet/phone value) produce a
|
|
41
|
+
// merged array whose sum is not necessarily 100 — desktop values for the
|
|
42
|
+
// non-overridden rows don't know about the tablet overrides. Normalize the
|
|
43
|
+
// final set so the CSS `grid-template-rows` stays valid.
|
|
44
|
+
const merged = section.cover_rows.map((row) => {
|
|
45
|
+
const override = overrides.find((o) => o._key === row._key);
|
|
46
|
+
if (override?.height_percent !== undefined) {
|
|
47
|
+
return { ...row, height_percent: override.height_percent };
|
|
48
|
+
}
|
|
49
|
+
return row;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const total = merged.reduce((acc, r) => acc + r.height_percent, 0);
|
|
53
|
+
if (Math.abs(total - 100) <= 0.5) return merged;
|
|
54
|
+
|
|
55
|
+
const normalized = normalizeRowHeights(merged.map((r) => r.height_percent));
|
|
56
|
+
return merged.map((r, i) => ({ ...r, height_percent: normalized[i] ?? r.height_percent }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function CoverSectionCanvas({
|
|
60
|
+
section,
|
|
61
|
+
onAddBlockTarget,
|
|
62
|
+
}: CoverSectionCanvasProps) {
|
|
63
|
+
const store = useBuilderStore();
|
|
64
|
+
const activeViewport = store.activeViewport || "desktop";
|
|
65
|
+
const previewMode = store.previewMode;
|
|
66
|
+
const [isSectionHovered, setIsSectionHovered] = useState(false);
|
|
67
|
+
|
|
68
|
+
const vhPixels = DEVICE_HEIGHTS[activeViewport];
|
|
69
|
+
const heightMultiplier = (parseInt(section.height || "100", 10) || 100) / 100;
|
|
70
|
+
const containerHeight = Math.round(vhPixels * heightMultiplier);
|
|
71
|
+
|
|
72
|
+
const bgImageUrl = section.background_type === "image" && section.background_image
|
|
73
|
+
? adminAssetUrl(section.background_image)
|
|
74
|
+
: null;
|
|
75
|
+
const bgVideoUrl = section.background_type === "video" && section.background_video
|
|
76
|
+
? adminAssetUrl(section.background_video)
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
const effectiveRows = useMemo(
|
|
80
|
+
() => getEffectiveCoverRows(section, activeViewport),
|
|
81
|
+
[section, activeViewport]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const virtualSectionsPerRow = useMemo(() => {
|
|
85
|
+
return effectiveRows.map((row, rowIndex) => {
|
|
86
|
+
const rowNumber = rowIndex + 1;
|
|
87
|
+
const rowColumnKeys = new Set(
|
|
88
|
+
section.columns.filter((c) => c.grid_row === rowNumber).map((c) => c._key),
|
|
89
|
+
);
|
|
90
|
+
const rowColumns = section.columns
|
|
91
|
+
.filter((c) => c.grid_row === rowNumber)
|
|
92
|
+
.map((c) => ({ ...c, grid_row: 1 }));
|
|
93
|
+
|
|
94
|
+
// Project the Cover's responsive overrides into this row's virtual section.
|
|
95
|
+
// Only keep column overrides whose `_key` belongs to this row, and remap their
|
|
96
|
+
// `grid_row` from the Cover's actual row number back to 1 (the virtual row).
|
|
97
|
+
// Without this, SectionV2Canvas calls `getEffectiveColumnsV2` against the virtual
|
|
98
|
+
// section and never sees the overrides — tablet/phone view falls back to the
|
|
99
|
+
// desktop layout even when overrides exist.
|
|
100
|
+
const projectOverridesForRow = (
|
|
101
|
+
overrides: ColumnOverride[] | undefined,
|
|
102
|
+
): ColumnOverride[] | undefined => {
|
|
103
|
+
if (!overrides) return undefined;
|
|
104
|
+
const filtered = overrides
|
|
105
|
+
.filter((o) => rowColumnKeys.has(o._key))
|
|
106
|
+
.map((o) =>
|
|
107
|
+
o.grid_row !== undefined ? { ...o, grid_row: 1 } : o,
|
|
108
|
+
);
|
|
109
|
+
return filtered.length ? filtered : undefined;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const virtualResponsive: PageSectionV2["responsive"] = {};
|
|
113
|
+
const tabletCols = projectOverridesForRow(section.responsive?.tablet?.columns);
|
|
114
|
+
const phoneCols = projectOverridesForRow(section.responsive?.phone?.columns);
|
|
115
|
+
if (tabletCols) virtualResponsive.tablet = { columns: tabletCols };
|
|
116
|
+
if (phoneCols) virtualResponsive.phone = { columns: phoneCols };
|
|
117
|
+
|
|
118
|
+
const virtualSection: PageSectionV2 = {
|
|
119
|
+
_type: "pageSectionV2",
|
|
120
|
+
_key: section._key,
|
|
121
|
+
section_type: "empty-v2",
|
|
122
|
+
columns: rowColumns,
|
|
123
|
+
settings: {
|
|
124
|
+
preset: "custom",
|
|
125
|
+
grid_columns: section.settings.grid_columns || 12,
|
|
126
|
+
col_gap: section.settings.col_gap ?? 20,
|
|
127
|
+
row_gap: section.settings.row_gap ?? 20,
|
|
128
|
+
},
|
|
129
|
+
...(Object.keys(virtualResponsive).length > 0
|
|
130
|
+
? { responsive: virtualResponsive }
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
return { row, rowNumber, virtualSection };
|
|
134
|
+
});
|
|
135
|
+
}, [effectiveRows, section]);
|
|
136
|
+
|
|
137
|
+
// Custom responsive updater for virtual per-row sections. Takes the responsive
|
|
138
|
+
// object that SectionV2Canvas built (with `grid_row: 1` for this row's columns
|
|
139
|
+
// and keys limited to this row) and merges it back into the Cover's flat
|
|
140
|
+
// column override list: remaps `grid_row: 1` back to `rowNumber`, preserves
|
|
141
|
+
// overrides for other rows, and preserves `cover_rows` + `settings` at the
|
|
142
|
+
// responsive level which are invisible to SectionV2Canvas.
|
|
143
|
+
const handleVirtualRowResponsiveUpdate = useCallback(
|
|
144
|
+
(rowNumber: number) =>
|
|
145
|
+
(_sectionKey: string, incoming: PageSectionV2["responsive"]): void => {
|
|
146
|
+
const rowColumnKeys = new Set(
|
|
147
|
+
section.columns.filter((c) => c.grid_row === rowNumber).map((c) => c._key),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const merged: CoverSection["responsive"] = {};
|
|
151
|
+
|
|
152
|
+
(["tablet", "phone"] as const).forEach((vp) => {
|
|
153
|
+
const existingVp = section.responsive?.[vp];
|
|
154
|
+
const incomingVp = incoming?.[vp];
|
|
155
|
+
|
|
156
|
+
// Columns for OTHER rows from the Cover's existing responsive stay as-is.
|
|
157
|
+
const otherRowOverrides = (existingVp?.columns || []).filter(
|
|
158
|
+
(o) => !rowColumnKeys.has(o._key),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Columns for THIS row come from the incoming virtual-section responsive,
|
|
162
|
+
// with grid_row remapped 1 → rowNumber.
|
|
163
|
+
const thisRowOverrides = (incomingVp?.columns || []).map((o) =>
|
|
164
|
+
o.grid_row !== undefined ? { ...o, grid_row: rowNumber } : o,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const mergedCols = [...otherRowOverrides, ...thisRowOverrides];
|
|
168
|
+
|
|
169
|
+
const vpOverride: NonNullable<CoverSection["responsive"]>[typeof vp] = {};
|
|
170
|
+
if (mergedCols.length > 0) vpOverride.columns = mergedCols;
|
|
171
|
+
// Preserve cover_rows + settings that the virtual section never sees.
|
|
172
|
+
if (existingVp?.cover_rows) vpOverride.cover_rows = existingVp.cover_rows;
|
|
173
|
+
if (existingVp?.settings) vpOverride.settings = existingVp.settings;
|
|
174
|
+
|
|
175
|
+
if (Object.keys(vpOverride).length > 0) {
|
|
176
|
+
merged[vp] = vpOverride;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const finalResponsive = Object.keys(merged).length > 0 ? merged : undefined;
|
|
181
|
+
// CoverSection and PageSectionV2 share the same `updateSectionV2Responsive`
|
|
182
|
+
// store action (it writes to any section by _key); the runtime shapes align
|
|
183
|
+
// on `{ tablet?, phone? }` — the extra `cover_rows` field on CoverSection
|
|
184
|
+
// is carried through without touching the V2-shaped write path.
|
|
185
|
+
store.updateSectionV2Responsive(
|
|
186
|
+
section._key,
|
|
187
|
+
finalResponsive as PageSectionV2["responsive"],
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
[section, store],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
className="relative"
|
|
196
|
+
style={{
|
|
197
|
+
borderRadius: 12,
|
|
198
|
+
border: `1.5px solid ${COVER_ACCENT}40`,
|
|
199
|
+
overflow: "visible",
|
|
200
|
+
}}
|
|
201
|
+
onMouseEnter={() => setIsSectionHovered(true)}
|
|
202
|
+
onMouseLeave={() => setIsSectionHovered(false)}
|
|
203
|
+
>
|
|
204
|
+
{/* Cover container — simulated viewport height. The previous top
|
|
205
|
+
header bar was removed; row settings now live in the section pill
|
|
206
|
+
(see SortableRow). */}
|
|
207
|
+
<div
|
|
208
|
+
className="relative"
|
|
209
|
+
style={{ height: containerHeight }}
|
|
210
|
+
>
|
|
211
|
+
{/* Background preview — image */}
|
|
212
|
+
{bgImageUrl && (
|
|
213
|
+
<div
|
|
214
|
+
className="absolute inset-0 bg-cover bg-center pointer-events-none"
|
|
215
|
+
style={{
|
|
216
|
+
backgroundImage: `url("${bgImageUrl}")`,
|
|
217
|
+
backgroundSize: section.background_size || "cover",
|
|
218
|
+
backgroundPosition: section.background_position || "center center",
|
|
219
|
+
opacity: 0.12,
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Background preview — video */}
|
|
225
|
+
{bgVideoUrl && (
|
|
226
|
+
<video
|
|
227
|
+
src={bgVideoUrl}
|
|
228
|
+
muted
|
|
229
|
+
playsInline
|
|
230
|
+
autoPlay
|
|
231
|
+
loop
|
|
232
|
+
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
|
233
|
+
style={{ opacity: 0.12 }}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{/* Background preview — solid color */}
|
|
238
|
+
{section.background_type === "color" && section.background_color && (
|
|
239
|
+
<div
|
|
240
|
+
className="absolute inset-0 pointer-events-none"
|
|
241
|
+
style={{ backgroundColor: section.background_color, opacity: 0.25 }}
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Overlay preview */}
|
|
246
|
+
{(section.background_overlay_opacity ?? 0) > 0 && (
|
|
247
|
+
<div
|
|
248
|
+
className="absolute inset-0 pointer-events-none"
|
|
249
|
+
style={{
|
|
250
|
+
backgroundColor: section.background_overlay_color || "#000000",
|
|
251
|
+
opacity: (section.background_overlay_opacity || 0) / 100 * 0.3,
|
|
252
|
+
overflow: "hidden",
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{/* Proportional rows.
|
|
258
|
+
*
|
|
259
|
+
* IMPORTANT — do NOT add `zIndex`, `isolate`, `transform`, `filter`,
|
|
260
|
+
* or any other property that establishes a new CSS stacking context
|
|
261
|
+
* on this wrapper. The column chrome (drag handle, delete button)
|
|
262
|
+
* inside each SectionV2Canvas/SortableColumn is absolutely positioned
|
|
263
|
+
* at z-[6] and must stack ABOVE the side pill (z-[5]) rendered by
|
|
264
|
+
* the parent SortableRow. If this wrapper creates a stacking context,
|
|
265
|
+
* the chrome gets trapped below the pill regardless of its local
|
|
266
|
+
* z-index — the whole subtree ends up at this wrapper's level in the
|
|
267
|
+
* parent stacking context.
|
|
268
|
+
*
|
|
269
|
+
* The rows visually stack above the absolute background/overlay divs
|
|
270
|
+
* naturally because they come LATER in the DOM and those bg divs
|
|
271
|
+
* don't have a positive z-index either.
|
|
272
|
+
*
|
|
273
|
+
* See `lib/builder/__tests__/section-visibility.test.ts` for the
|
|
274
|
+
* related nav-colour logic and this file's git history for the
|
|
275
|
+
* original bug (Session 178+: column chrome clipped in Cover).
|
|
276
|
+
*/}
|
|
277
|
+
<div
|
|
278
|
+
className="relative flex flex-col h-full"
|
|
279
|
+
>
|
|
280
|
+
{virtualSectionsPerRow.map(({ row, rowNumber, virtualSection }, rowIndex) => {
|
|
281
|
+
const alignMap = { start: "flex-start", center: "center", end: "flex-end" };
|
|
282
|
+
const isLastRow = rowIndex === effectiveRows.length - 1;
|
|
283
|
+
const hasColumns = virtualSection.columns.length > 0;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
key={row._key}
|
|
288
|
+
className="relative"
|
|
289
|
+
style={{
|
|
290
|
+
height: `${row.height_percent}%`,
|
|
291
|
+
minHeight: 0,
|
|
292
|
+
display: "flex",
|
|
293
|
+
flexDirection: "column",
|
|
294
|
+
justifyContent: alignMap[row.vertical_align] || "flex-start",
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{hasColumns ? (
|
|
298
|
+
/* V2 grid for this row's columns — overflow visible so the
|
|
299
|
+
column chrome (drag handle, delete button) that translates
|
|
300
|
+
outside column bounds doesn't get clipped. The public
|
|
301
|
+
renderer still clips at the <section> level so overflowing
|
|
302
|
+
content never leaves the cover on the live site. */
|
|
303
|
+
<div className="flex-1 min-h-0 flex flex-col" style={{ overflow: "visible" }}>
|
|
304
|
+
<SectionV2Canvas
|
|
305
|
+
section={virtualSection}
|
|
306
|
+
onAddBlockTarget={onAddBlockTarget}
|
|
307
|
+
fillHeight
|
|
308
|
+
gridRowOffset={rowNumber - 1}
|
|
309
|
+
onUpdateResponsive={handleVirtualRowResponsiveUpdate(rowNumber)}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
/* Empty row: direct + Add Column button with correct gridRow */
|
|
314
|
+
<div
|
|
315
|
+
className="flex-1 min-h-0 flex items-center justify-center"
|
|
316
|
+
style={{
|
|
317
|
+
border: isSectionHovered ? `1.5px dashed ${COVER_ACCENT}30` : "1.5px dashed transparent",
|
|
318
|
+
borderRadius: 6,
|
|
319
|
+
margin: 4,
|
|
320
|
+
transition: "border-color 150ms",
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
<button
|
|
324
|
+
onClick={(e) => {
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
store.addColumnV2(section._key, rowNumber, 1, section.settings.grid_columns || 12);
|
|
327
|
+
}}
|
|
328
|
+
className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
|
|
329
|
+
style={{
|
|
330
|
+
padding: "5px 16px",
|
|
331
|
+
background: `rgba(53, 128, 249, 0.10)`,
|
|
332
|
+
color: "#3580f9",
|
|
333
|
+
border: "1.5px dashed rgba(53, 128, 249, 0.4)",
|
|
334
|
+
opacity: isSectionHovered ? 1 : 0,
|
|
335
|
+
pointerEvents: isSectionHovered ? "auto" : "none",
|
|
336
|
+
transition: "opacity 150ms",
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
+ Add Column
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* Resize handle between this row and the next */}
|
|
345
|
+
{!isLastRow && (
|
|
346
|
+
<CoverRowResizeHandle
|
|
347
|
+
sectionKey={section._key}
|
|
348
|
+
handleIndex={rowIndex}
|
|
349
|
+
abovePercent={row.height_percent}
|
|
350
|
+
belowPercent={effectiveRows[rowIndex + 1]?.height_percent ?? 0}
|
|
351
|
+
containerHeight={containerHeight}
|
|
352
|
+
isSectionHovered={isSectionHovered}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
})}
|
|
358
|
+
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|