@morphika/andami 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/app/admin/pages/[slug]/page.tsx +39 -2
- package/components/blocks/BlockRenderer.tsx +0 -7
- package/components/blocks/CoverSectionRenderer.tsx +295 -0
- package/components/blocks/ImageBlockRenderer.tsx +12 -10
- package/components/blocks/PageRenderer.tsx +13 -9
- package/components/blocks/VideoBlockRenderer.tsx +11 -6
- package/components/builder/BlockLivePreview.tsx +0 -5
- package/components/builder/BlockTypePicker.tsx +0 -1
- package/components/builder/ColorSwatchPicker.tsx +2 -2
- package/components/builder/CoverRowResizeHandle.tsx +180 -0
- package/components/builder/CoverSectionCanvas.tsx +260 -0
- package/components/builder/ReadOnlyFrame.tsx +127 -3
- package/components/builder/SectionTypePicker.tsx +29 -0
- package/components/builder/SectionV2Canvas.tsx +4 -1
- package/components/builder/SectionV2Column.tsx +15 -20
- package/components/builder/SettingsPanel.tsx +14 -0
- package/components/builder/SortableRow.tsx +7 -21
- package/components/builder/blockStyles.tsx +13 -14
- package/components/builder/editors/ImageBlockEditor.tsx +1 -0
- package/components/builder/editors/VideoBlockEditor.tsx +1 -0
- package/components/builder/editors/index.ts +0 -1
- package/components/builder/index.ts +1 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
- package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
- package/components/builder/live-preview/RichTextEditor.tsx +23 -2
- package/components/builder/live-preview/index.ts +0 -1
- package/components/builder/settings-panel/BlockSettings.tsx +0 -7
- package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
- package/lib/animation/enter-types.ts +0 -1
- package/lib/animation/hover-effect-types.ts +0 -1
- package/lib/builder/defaults.ts +43 -22
- package/lib/builder/serializer/normalizers.ts +34 -1
- package/lib/builder/serializer/serializers.ts +39 -2
- package/lib/builder/store-blocks.ts +11 -3
- package/lib/builder/store-cover.ts +220 -0
- package/lib/builder/store-helpers.ts +81 -4
- package/lib/builder/store-sections.ts +12 -2
- package/lib/builder/store.ts +11 -2
- package/lib/builder/types.ts +15 -2
- package/lib/sanity/queries.ts +18 -4
- package/lib/sanity/types.ts +81 -45
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/imageBlock.ts +1 -0
- package/sanity/schemas/blocks/index.ts +1 -2
- package/sanity/schemas/blocks/videoBlock.ts +1 -0
- package/sanity/schemas/index.ts +5 -3
- package/sanity/schemas/objects/coverSection.ts +317 -0
- package/sanity/schemas/objects/parallaxSlide.ts +0 -1
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +0 -1
- package/components/blocks/CoverBlockRenderer.tsx +0 -261
- package/components/builder/editors/CoverBlockEditor.tsx +0 -550
- package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
- package/sanity/schemas/blocks/coverBlock.ts +0 -229
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CoverSectionSettings — Settings panel for a Cover Section.
|
|
5
|
+
*
|
|
6
|
+
* Displays:
|
|
7
|
+
* - Background type segmented control (Image / Video)
|
|
8
|
+
* - AssetBrowser picker for image or video
|
|
9
|
+
* - Background position / size dropdowns
|
|
10
|
+
* - Overlay color + overlay opacity slider
|
|
11
|
+
* - Section height selector (100vh / 80vh / 50vh)
|
|
12
|
+
* - Row list with percentages and vertical align
|
|
13
|
+
* - Add/Remove row controls
|
|
14
|
+
*
|
|
15
|
+
* Layout and Animation tabs are delegated to SectionV2LayoutTab / SectionV2AnimationTab
|
|
16
|
+
* via a virtual PageSectionV2 (same pattern as parallax slides).
|
|
17
|
+
*
|
|
18
|
+
* Session 176: Cover Sections — Phase 6 (Settings Panel).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
22
|
+
import type { CoverSection } from "../../../lib/sanity/types";
|
|
23
|
+
import {
|
|
24
|
+
BackgroundIcon,
|
|
25
|
+
OverlayIcon,
|
|
26
|
+
SpacingIcon,
|
|
27
|
+
GridGapsIcon,
|
|
28
|
+
} from "../editors/section-icons";
|
|
29
|
+
import {
|
|
30
|
+
SettingsField,
|
|
31
|
+
SettingsSection,
|
|
32
|
+
} from "../editors/shared";
|
|
33
|
+
import { AssetPathInput } from "../editors/shared";
|
|
34
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
35
|
+
import StaggerSettings from "../editors/StaggerSettings";
|
|
36
|
+
|
|
37
|
+
const BG_POSITION_OPTIONS = [
|
|
38
|
+
{ value: "center center", label: "Center" },
|
|
39
|
+
{ value: "top center", label: "Top" },
|
|
40
|
+
{ value: "bottom center", label: "Bottom" },
|
|
41
|
+
{ value: "center left", label: "Left" },
|
|
42
|
+
{ value: "center right", label: "Right" },
|
|
43
|
+
{ value: "top left", label: "Top Left" },
|
|
44
|
+
{ value: "top right", label: "Top Right" },
|
|
45
|
+
{ value: "bottom left", label: "Bottom Left" },
|
|
46
|
+
{ value: "bottom right", label: "Bottom Right" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const BG_SIZE_OPTIONS = [
|
|
50
|
+
{ value: "cover", label: "Cover" },
|
|
51
|
+
{ value: "contain", label: "Contain" },
|
|
52
|
+
{ value: "auto", label: "Auto" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const HEIGHT_OPTIONS = [
|
|
56
|
+
{ value: "100vh", label: "Full Viewport (100vh)" },
|
|
57
|
+
{ value: "80vh", label: "80% Viewport (80vh)" },
|
|
58
|
+
{ value: "50vh", label: "50% Viewport (50vh)" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const ALIGN_OPTIONS = [
|
|
62
|
+
{ value: "start", label: "Top" },
|
|
63
|
+
{ value: "center", label: "Center" },
|
|
64
|
+
{ value: "end", label: "Bottom" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const SELECT_CLASS = "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
|
|
68
|
+
|
|
69
|
+
interface CoverSectionSettingsProps {
|
|
70
|
+
section: CoverSection;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
|
|
74
|
+
const store = useBuilderStore();
|
|
75
|
+
const paletteSwatches = usePaletteSwatches();
|
|
76
|
+
|
|
77
|
+
const bgType = section.background_type || "image";
|
|
78
|
+
const bgPosition = section.background_position || "center center";
|
|
79
|
+
const bgSize = section.background_size || "cover";
|
|
80
|
+
const overlayColor = section.background_overlay_color || "#000000";
|
|
81
|
+
const overlayOpacity = section.background_overlay_opacity ?? 0;
|
|
82
|
+
|
|
83
|
+
const updateBg = (fields: Partial<Pick<CoverSection,
|
|
84
|
+
"background_type" | "background_image" | "background_video" |
|
|
85
|
+
"background_position" | "background_size" |
|
|
86
|
+
"background_overlay_color" | "background_overlay_opacity"
|
|
87
|
+
>>) => {
|
|
88
|
+
store.updateCoverBackground(section._key, fields);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
{/* Background */}
|
|
94
|
+
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
95
|
+
<SettingsField label="Type">
|
|
96
|
+
<div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
|
|
97
|
+
{(["image", "video"] as const).map((type) => (
|
|
98
|
+
<button
|
|
99
|
+
key={type}
|
|
100
|
+
onClick={() => updateBg({ background_type: type })}
|
|
101
|
+
className={`flex-1 py-1.5 rounded-md text-[11px] font-medium transition-all ${
|
|
102
|
+
bgType === type
|
|
103
|
+
? "bg-white text-neutral-900 shadow-sm border border-[#e5e5e5]"
|
|
104
|
+
: "text-neutral-400 hover:text-neutral-500"
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{type === "image" ? "Image" : "Video"}
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</SettingsField>
|
|
112
|
+
|
|
113
|
+
<SettingsField label={bgType === "image" ? "Image" : "Video"}>
|
|
114
|
+
<AssetPathInput
|
|
115
|
+
value={bgType === "image" ? (section.background_image || "") : (section.background_video || "")}
|
|
116
|
+
onChange={(path) => {
|
|
117
|
+
if (bgType === "image") {
|
|
118
|
+
updateBg({ background_image: path });
|
|
119
|
+
} else {
|
|
120
|
+
updateBg({ background_video: path });
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
filterType={bgType === "image" ? "image" : "video"}
|
|
124
|
+
placeholder={bgType === "image" ? "path/to/image.jpg" : "path/to/video.mp4"}
|
|
125
|
+
/>
|
|
126
|
+
</SettingsField>
|
|
127
|
+
|
|
128
|
+
<SettingsField label="Position">
|
|
129
|
+
<select
|
|
130
|
+
value={bgPosition}
|
|
131
|
+
onChange={(e) => updateBg({ background_position: e.target.value })}
|
|
132
|
+
className={SELECT_CLASS}
|
|
133
|
+
>
|
|
134
|
+
{BG_POSITION_OPTIONS.map((opt) => (
|
|
135
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
136
|
+
))}
|
|
137
|
+
</select>
|
|
138
|
+
</SettingsField>
|
|
139
|
+
|
|
140
|
+
<SettingsField label="Size">
|
|
141
|
+
<select
|
|
142
|
+
value={bgSize}
|
|
143
|
+
onChange={(e) => updateBg({ background_size: e.target.value as CoverSection["background_size"] })}
|
|
144
|
+
className={SELECT_CLASS}
|
|
145
|
+
>
|
|
146
|
+
{BG_SIZE_OPTIONS.map((opt) => (
|
|
147
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
148
|
+
))}
|
|
149
|
+
</select>
|
|
150
|
+
</SettingsField>
|
|
151
|
+
</SettingsSection>
|
|
152
|
+
|
|
153
|
+
{/* Overlay */}
|
|
154
|
+
<SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
|
|
155
|
+
<SettingsField label="Color">
|
|
156
|
+
<ColorSwatchPicker
|
|
157
|
+
value={overlayColor}
|
|
158
|
+
onChange={(val) => updateBg({ background_overlay_color: typeof val === "string" ? val : "" })}
|
|
159
|
+
swatches={paletteSwatches}
|
|
160
|
+
/>
|
|
161
|
+
</SettingsField>
|
|
162
|
+
|
|
163
|
+
<SettingsField label="Opacity">
|
|
164
|
+
<div className="flex items-center gap-2">
|
|
165
|
+
<input
|
|
166
|
+
type="range"
|
|
167
|
+
min={0}
|
|
168
|
+
max={100}
|
|
169
|
+
step={5}
|
|
170
|
+
value={overlayOpacity}
|
|
171
|
+
onChange={(e) => updateBg({ background_overlay_opacity: parseInt(e.target.value) })}
|
|
172
|
+
className="flex-1 accent-[#076bff]"
|
|
173
|
+
/>
|
|
174
|
+
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
175
|
+
{overlayOpacity}%
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
</SettingsField>
|
|
179
|
+
</SettingsSection>
|
|
180
|
+
|
|
181
|
+
{/* Section Height */}
|
|
182
|
+
<SettingsSection title="Height" defaultOpen icon={<SpacingIcon />}>
|
|
183
|
+
<SettingsField label="Height">
|
|
184
|
+
<select
|
|
185
|
+
value={section.height}
|
|
186
|
+
onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
|
|
187
|
+
className={SELECT_CLASS}
|
|
188
|
+
>
|
|
189
|
+
{HEIGHT_OPTIONS.map((opt) => (
|
|
190
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
191
|
+
))}
|
|
192
|
+
</select>
|
|
193
|
+
</SettingsField>
|
|
194
|
+
</SettingsSection>
|
|
195
|
+
|
|
196
|
+
{/* Rows */}
|
|
197
|
+
<SettingsSection title="Rows" defaultOpen icon={<GridGapsIcon />}>
|
|
198
|
+
<div className="space-y-2">
|
|
199
|
+
{section.cover_rows.map((row, i) => (
|
|
200
|
+
<div
|
|
201
|
+
key={row._key}
|
|
202
|
+
className="flex items-center gap-2 rounded-lg bg-[#f5f5f5] px-2.5 py-2"
|
|
203
|
+
>
|
|
204
|
+
<span className="text-[10px] font-semibold text-neutral-400 w-6 shrink-0">
|
|
205
|
+
R{i + 1}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="text-xs text-neutral-700 font-medium tabular-nums w-10">
|
|
208
|
+
{row.height_percent}%
|
|
209
|
+
</span>
|
|
210
|
+
<select
|
|
211
|
+
value={row.vertical_align}
|
|
212
|
+
onChange={(e) =>
|
|
213
|
+
store.updateCoverRowAlign(
|
|
214
|
+
section._key,
|
|
215
|
+
row._key,
|
|
216
|
+
e.target.value as "start" | "center" | "end"
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
className="flex-1 rounded-md border border-transparent bg-white px-2 py-1 text-[11px] text-neutral-700 outline-none hover:border-[#e5e5e5] focus:border-[#076bff]"
|
|
220
|
+
>
|
|
221
|
+
{ALIGN_OPTIONS.map((opt) => (
|
|
222
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
223
|
+
))}
|
|
224
|
+
</select>
|
|
225
|
+
{section.cover_rows.length > 1 && (
|
|
226
|
+
<button
|
|
227
|
+
onClick={() => store.removeCoverRow(section._key, row._key)}
|
|
228
|
+
className="text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
|
|
229
|
+
title="Remove row"
|
|
230
|
+
>
|
|
231
|
+
✕
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{section.cover_rows.length < 5 && (
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => store.addCoverRow(section._key)}
|
|
241
|
+
className="w-full mt-2 rounded-lg border border-dashed border-neutral-300 py-2 text-[11px] font-medium text-neutral-400 hover:text-neutral-600 hover:border-neutral-400 transition-colors"
|
|
242
|
+
>
|
|
243
|
+
+ Add Row
|
|
244
|
+
</button>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
<p className="text-[10px] text-neutral-400 leading-snug px-0.5 mt-2">
|
|
248
|
+
Drag the handles between rows in the canvas to resize. Heights always sum to 100%.
|
|
249
|
+
</p>
|
|
250
|
+
</SettingsSection>
|
|
251
|
+
|
|
252
|
+
{/* Grid Gaps */}
|
|
253
|
+
<SettingsSection title="Grid" defaultOpen={false} icon={<GridGapsIcon />}>
|
|
254
|
+
<SettingsField label="Col Gap">
|
|
255
|
+
<div className="flex items-center gap-2">
|
|
256
|
+
<input
|
|
257
|
+
type="range"
|
|
258
|
+
min={0}
|
|
259
|
+
max={60}
|
|
260
|
+
step={2}
|
|
261
|
+
value={section.settings.col_gap ?? 20}
|
|
262
|
+
onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
|
|
263
|
+
className="flex-1 accent-[#076bff]"
|
|
264
|
+
/>
|
|
265
|
+
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
266
|
+
{section.settings.col_gap ?? 20}px
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
</SettingsField>
|
|
270
|
+
|
|
271
|
+
<SettingsField label="Row Gap">
|
|
272
|
+
<div className="flex items-center gap-2">
|
|
273
|
+
<input
|
|
274
|
+
type="range"
|
|
275
|
+
min={0}
|
|
276
|
+
max={60}
|
|
277
|
+
step={2}
|
|
278
|
+
value={section.settings.row_gap ?? 20}
|
|
279
|
+
onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
|
|
280
|
+
className="flex-1 accent-[#076bff]"
|
|
281
|
+
/>
|
|
282
|
+
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
283
|
+
{section.settings.row_gap ?? 20}px
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
</SettingsField>
|
|
287
|
+
</SettingsSection>
|
|
288
|
+
|
|
289
|
+
{/* Stagger */}
|
|
290
|
+
<StaggerSettings
|
|
291
|
+
stagger={section.settings.stagger}
|
|
292
|
+
onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
|
|
293
|
+
/>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -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,
|