@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.
Files changed (122) hide show
  1. package/README.md +151 -36
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. 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 rowColumns = section.columns
88
- .filter((c) => c.grid_row === rowNumber)
89
- .map((c) => ({ ...c, grid_row: 1 }));
90
- const virtualSection: PageSectionV2 = {
91
- _type: "pageSectionV2",
92
- _key: section._key,
93
- section_type: "empty-v2",
94
- columns: rowColumns,
95
- settings: {
96
- preset: "custom",
97
- grid_columns: section.settings.grid_columns || 12,
98
- col_gap: section.settings.col_gap ?? 20,
99
- row_gap: section.settings.row_gap ?? 20,
100
- },
101
- };
102
- return { row, rowNumber, virtualSection };
103
- });
104
- }, [effectiveRows, section]);
105
-
106
- return (
107
- <div
108
- className="relative"
109
- style={{
110
- borderRadius: 12,
111
- border: `1.5px solid ${COVER_ACCENT}40`,
112
- overflow: "visible",
113
- }}
114
- onMouseEnter={() => setIsSectionHovered(true)}
115
- onMouseLeave={() => setIsSectionHovered(false)}
116
- >
117
- {/* Cover container — simulated viewport height. The previous top
118
- header bar was removed; row settings now live in the section pill
119
- (see SortableRow). */}
120
- <div
121
- className="relative"
122
- style={{ height: containerHeight }}
123
- >
124
- {/* Background preview — image */}
125
- {bgImageUrl && (
126
- <div
127
- className="absolute inset-0 bg-cover bg-center pointer-events-none"
128
- style={{
129
- backgroundImage: `url("${bgImageUrl}")`,
130
- backgroundSize: section.background_size || "cover",
131
- backgroundPosition: section.background_position || "center center",
132
- opacity: 0.12,
133
- }}
134
- />
135
- )}
136
-
137
- {/* Background preview video */}
138
- {bgVideoUrl && (
139
- <video
140
- src={bgVideoUrl}
141
- muted
142
- playsInline
143
- autoPlay
144
- loop
145
- className="absolute inset-0 w-full h-full object-cover pointer-events-none"
146
- style={{ opacity: 0.12 }}
147
- />
148
- )}
149
-
150
- {/* Background preview solid color */}
151
- {section.background_type === "color" && section.background_color && (
152
- <div
153
- className="absolute inset-0 pointer-events-none"
154
- style={{ backgroundColor: section.background_color, opacity: 0.25 }}
155
- />
156
- )}
157
-
158
- {/* Overlay preview */}
159
- {(section.background_overlay_opacity ?? 0) > 0 && (
160
- <div
161
- className="absolute inset-0 pointer-events-none"
162
- style={{
163
- backgroundColor: section.background_overlay_color || "#000000",
164
- opacity: (section.background_overlay_opacity || 0) / 100 * 0.3,
165
- overflow: "hidden",
166
- }}
167
- />
168
- )}
169
-
170
- {/* Proportional rows.
171
- *
172
- * IMPORTANT do NOT add `zIndex`, `isolate`, `transform`, `filter`,
173
- * or any other property that establishes a new CSS stacking context
174
- * on this wrapper. The column chrome (drag handle, delete button)
175
- * inside each SectionV2Canvas/SortableColumn is absolutely positioned
176
- * at z-[6] and must stack ABOVE the side pill (z-[5]) rendered by
177
- * the parent SortableRow. If this wrapper creates a stacking context,
178
- * the chrome gets trapped below the pill regardless of its local
179
- * z-index — the whole subtree ends up at this wrapper's level in the
180
- * parent stacking context.
181
- *
182
- * The rows visually stack above the absolute background/overlay divs
183
- * naturally because they come LATER in the DOM and those bg divs
184
- * don't have a positive z-index either.
185
- *
186
- * See `lib/builder/__tests__/section-visibility.test.ts` for the
187
- * related nav-colour logic and this file's git history for the
188
- * original bug (Session 178+: column chrome clipped in Cover).
189
- */}
190
- <div
191
- className="relative flex flex-col h-full"
192
- >
193
- {virtualSectionsPerRow.map(({ row, rowNumber, virtualSection }, rowIndex) => {
194
- const alignMap = { start: "flex-start", center: "center", end: "flex-end" };
195
- const isLastRow = rowIndex === effectiveRows.length - 1;
196
- const hasColumns = virtualSection.columns.length > 0;
197
-
198
- return (
199
- <div
200
- key={row._key}
201
- className="relative"
202
- style={{
203
- height: `${row.height_percent}%`,
204
- minHeight: 0,
205
- display: "flex",
206
- flexDirection: "column",
207
- justifyContent: alignMap[row.vertical_align] || "flex-start",
208
- }}
209
- >
210
- {hasColumns ? (
211
- /* V2 grid for this row's columns overflow visible so the
212
- column chrome (drag handle, delete button) that translates
213
- outside column bounds doesn't get clipped. The public
214
- renderer still clips at the <section> level so overflowing
215
- content never leaves the cover on the live site. */
216
- <div className="flex-1 min-h-0 flex flex-col" style={{ overflow: "visible" }}>
217
- <SectionV2Canvas
218
- section={virtualSection}
219
- onAddBlockTarget={onAddBlockTarget}
220
- fillHeight
221
- gridRowOffset={rowNumber - 1}
222
- />
223
- </div>
224
- ) : (
225
- /* Empty row: direct + Add Column button with correct gridRow */
226
- <div
227
- className="flex-1 min-h-0 flex items-center justify-center"
228
- style={{
229
- border: isSectionHovered ? `1.5px dashed ${COVER_ACCENT}30` : "1.5px dashed transparent",
230
- borderRadius: 6,
231
- margin: 4,
232
- transition: "border-color 150ms",
233
- }}
234
- >
235
- <button
236
- onClick={(e) => {
237
- e.stopPropagation();
238
- store.addColumnV2(section._key, rowNumber, 1, section.settings.grid_columns || 12);
239
- }}
240
- className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
241
- style={{
242
- padding: "5px 16px",
243
- background: `rgba(71, 148, 226, 0.10)`,
244
- color: "#4794e2",
245
- border: "1px dashed rgba(71, 148, 226, 0.4)",
246
- opacity: isSectionHovered ? 1 : 0,
247
- pointerEvents: isSectionHovered ? "auto" : "none",
248
- transition: "opacity 150ms",
249
- }}
250
- >
251
- + Add Column
252
- </button>
253
- </div>
254
- )}
255
-
256
- {/* Resize handle between this row and the next */}
257
- {!isLastRow && (
258
- <CoverRowResizeHandle
259
- sectionKey={section._key}
260
- handleIndex={rowIndex}
261
- abovePercent={row.height_percent}
262
- belowPercent={effectiveRows[rowIndex + 1]?.height_percent ?? 0}
263
- containerHeight={containerHeight}
264
- isSectionHovered={isSectionHovered}
265
- />
266
- )}
267
- </div>
268
- );
269
- })}
270
-
271
- </div>
272
- </div>
273
- </div>
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 previewimage */}
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
+ }