@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
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
|
-
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
5
|
-
import type { CoverBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
6
|
-
import {
|
|
7
|
-
ContentIcon,
|
|
8
|
-
CTAButtonIcon,
|
|
9
|
-
CoverBackgroundIcon,
|
|
10
|
-
CoverEffectsIcon,
|
|
11
|
-
LayoutIcon,
|
|
12
|
-
AppearanceIcon,
|
|
13
|
-
} from "./section-icons";
|
|
14
|
-
import {
|
|
15
|
-
SettingsField,
|
|
16
|
-
SettingsSection,
|
|
17
|
-
StyledInput,
|
|
18
|
-
StyledCheckbox,
|
|
19
|
-
AssetPathInput,
|
|
20
|
-
ViewportBadge,
|
|
21
|
-
ResponsiveField,
|
|
22
|
-
useActiveViewport,
|
|
23
|
-
SELECT_CLASS,
|
|
24
|
-
} from "./shared";
|
|
25
|
-
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
26
|
-
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
27
|
-
import type { ColorField } from "../../../lib/sanity/types";
|
|
28
|
-
|
|
29
|
-
interface Props {
|
|
30
|
-
block: CoverBlock;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ============================================
|
|
34
|
-
// 9-point position grid (Semplice-style)
|
|
35
|
-
// ============================================
|
|
36
|
-
|
|
37
|
-
const POSITIONS = [
|
|
38
|
-
{ label: "↖", value: "top left" },
|
|
39
|
-
{ label: "↑", value: "top center" },
|
|
40
|
-
{ label: "↗", value: "top right" },
|
|
41
|
-
{ label: "←", value: "center left" },
|
|
42
|
-
{ label: "·", value: "center center" },
|
|
43
|
-
{ label: "→", value: "center right" },
|
|
44
|
-
{ label: "↙", value: "bottom left" },
|
|
45
|
-
{ label: "↓", value: "bottom center" },
|
|
46
|
-
{ label: "↘", value: "bottom right" },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
function PositionGrid({
|
|
50
|
-
value,
|
|
51
|
-
onChange,
|
|
52
|
-
}: {
|
|
53
|
-
value: string;
|
|
54
|
-
onChange: (v: string) => void;
|
|
55
|
-
}) {
|
|
56
|
-
return (
|
|
57
|
-
<div className="grid grid-cols-3 gap-0.5 w-[84px]">
|
|
58
|
-
{POSITIONS.map((pos) => (
|
|
59
|
-
<button
|
|
60
|
-
key={pos.value}
|
|
61
|
-
onClick={() => onChange(pos.value)}
|
|
62
|
-
className={`w-7 h-7 rounded text-[10px] flex items-center justify-center transition-colors ${
|
|
63
|
-
value === pos.value
|
|
64
|
-
? "bg-[#076bff] text-white"
|
|
65
|
-
: "bg-neutral-100 text-neutral-400 hover:bg-neutral-200"
|
|
66
|
-
}`}
|
|
67
|
-
title={pos.value}
|
|
68
|
-
>
|
|
69
|
-
{pos.label}
|
|
70
|
-
</button>
|
|
71
|
-
))}
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ============================================
|
|
77
|
-
// 3x3 content alignment grid
|
|
78
|
-
// ============================================
|
|
79
|
-
|
|
80
|
-
function ContentAlignGrid({
|
|
81
|
-
alignH,
|
|
82
|
-
alignV,
|
|
83
|
-
onChangeH,
|
|
84
|
-
onChangeV,
|
|
85
|
-
}: {
|
|
86
|
-
alignH: string;
|
|
87
|
-
alignV: string;
|
|
88
|
-
onChangeH: (v: "left" | "center" | "right") => void;
|
|
89
|
-
onChangeV: (v: "top" | "center" | "bottom") => void;
|
|
90
|
-
}) {
|
|
91
|
-
const vOptions: Array<{ label: string; value: "top" | "center" | "bottom" }> = [
|
|
92
|
-
{ label: "T", value: "top" },
|
|
93
|
-
{ label: "M", value: "center" },
|
|
94
|
-
{ label: "B", value: "bottom" },
|
|
95
|
-
];
|
|
96
|
-
const hOptions: Array<{ label: string; value: "left" | "center" | "right" }> = [
|
|
97
|
-
{ label: "L", value: "left" },
|
|
98
|
-
{ label: "C", value: "center" },
|
|
99
|
-
{ label: "R", value: "right" },
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div className="grid grid-cols-3 gap-0.5 w-[84px]">
|
|
104
|
-
{vOptions.map((v) =>
|
|
105
|
-
hOptions.map((h) => {
|
|
106
|
-
const isActive = alignH === h.value && alignV === v.value;
|
|
107
|
-
return (
|
|
108
|
-
<button
|
|
109
|
-
key={`${v.value}-${h.value}`}
|
|
110
|
-
onClick={() => {
|
|
111
|
-
onChangeH(h.value);
|
|
112
|
-
onChangeV(v.value);
|
|
113
|
-
}}
|
|
114
|
-
className={`w-7 h-7 rounded flex items-center justify-center transition-colors ${
|
|
115
|
-
isActive
|
|
116
|
-
? "bg-[#076bff] text-white"
|
|
117
|
-
: "bg-neutral-100 text-neutral-400 hover:bg-neutral-200"
|
|
118
|
-
}`}
|
|
119
|
-
title={`${v.value} ${h.value}`}
|
|
120
|
-
>
|
|
121
|
-
<div
|
|
122
|
-
className={`w-1.5 h-1.5 rounded-full ${
|
|
123
|
-
isActive ? "bg-white" : "bg-neutral-400"
|
|
124
|
-
}`}
|
|
125
|
-
/>
|
|
126
|
-
</button>
|
|
127
|
-
);
|
|
128
|
-
})
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ============================================
|
|
135
|
-
// Main Editor
|
|
136
|
-
// ============================================
|
|
137
|
-
|
|
138
|
-
export default function CoverBlockEditor({ block }: Props) {
|
|
139
|
-
const store = useBuilderStore();
|
|
140
|
-
const viewport = useActiveViewport();
|
|
141
|
-
const paletteSwatches = usePaletteSwatches();
|
|
142
|
-
const cta = block.cta_button || {};
|
|
143
|
-
|
|
144
|
-
const snapshotOnFocus = () => store._pushSnapshot();
|
|
145
|
-
|
|
146
|
-
// Responsive-aware update
|
|
147
|
-
const updateResponsive = (property: string, value: unknown) => {
|
|
148
|
-
if (viewport === "desktop") {
|
|
149
|
-
store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
150
|
-
} else {
|
|
151
|
-
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
152
|
-
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const updateResponsiveDebounced = (property: string, value: unknown) => {
|
|
157
|
-
if (viewport === "desktop") {
|
|
158
|
-
store.updateBlockDebounced(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
159
|
-
} else {
|
|
160
|
-
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
161
|
-
store.updateBlockDebounced(block._key, overrides as Partial<ContentBlock>);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const resetOverride = (property: string) => {
|
|
166
|
-
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
167
|
-
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// Direct update (base block, not responsive)
|
|
171
|
-
const update = (updates: Partial<CoverBlock>) => {
|
|
172
|
-
store.updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const updateDebounced = (updates: Partial<CoverBlock>) => {
|
|
176
|
-
store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// Effective values for the active viewport
|
|
180
|
-
const effectiveHeight = getEffectiveValue<string>(
|
|
181
|
-
block as ContentBlock, viewport, "height", block.height || "100vh"
|
|
182
|
-
);
|
|
183
|
-
const effectiveCustomHeight = getEffectiveValue<string>(
|
|
184
|
-
block as ContentBlock, viewport, "custom_height", block.custom_height || ""
|
|
185
|
-
);
|
|
186
|
-
const effectiveAlignH = getEffectiveValue<string>(
|
|
187
|
-
block as ContentBlock, viewport, "content_align_h", block.content_align_h || "center"
|
|
188
|
-
);
|
|
189
|
-
const effectiveAlignV = getEffectiveValue<string>(
|
|
190
|
-
block as ContentBlock, viewport, "content_align_v", block.content_align_v || "center"
|
|
191
|
-
);
|
|
192
|
-
const effectiveMaxWidth = getEffectiveValue<string>(
|
|
193
|
-
block as ContentBlock, viewport, "content_max_width", block.content_max_width || "800px"
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<>
|
|
198
|
-
<ViewportBadge />
|
|
199
|
-
|
|
200
|
-
{/* ========== CONTENT ========== */}
|
|
201
|
-
<SettingsSection title="Content" defaultOpen icon={<ContentIcon />}>
|
|
202
|
-
<SettingsField label="Headline">
|
|
203
|
-
<StyledInput
|
|
204
|
-
value={block.headline || ""}
|
|
205
|
-
onFocus={snapshotOnFocus}
|
|
206
|
-
onChange={(v) => updateDebounced({ headline: v })}
|
|
207
|
-
placeholder="Page Headline"
|
|
208
|
-
/>
|
|
209
|
-
</SettingsField>
|
|
210
|
-
|
|
211
|
-
<SettingsField label="Subheadline">
|
|
212
|
-
<StyledInput
|
|
213
|
-
value={block.subheadline || ""}
|
|
214
|
-
onFocus={snapshotOnFocus}
|
|
215
|
-
onChange={(v) => updateDebounced({ subheadline: v })}
|
|
216
|
-
placeholder="Supporting text"
|
|
217
|
-
/>
|
|
218
|
-
</SettingsField>
|
|
219
|
-
</SettingsSection>
|
|
220
|
-
|
|
221
|
-
{/* ========== CTA BUTTON ========== */}
|
|
222
|
-
<SettingsSection title="CTA Button" icon={<CTAButtonIcon />}>
|
|
223
|
-
<SettingsField label="Button Text">
|
|
224
|
-
<StyledInput
|
|
225
|
-
value={cta.text || ""}
|
|
226
|
-
onFocus={snapshotOnFocus}
|
|
227
|
-
onChange={(v) =>
|
|
228
|
-
updateDebounced({ cta_button: { ...cta, text: v } })
|
|
229
|
-
}
|
|
230
|
-
placeholder="View Project"
|
|
231
|
-
/>
|
|
232
|
-
</SettingsField>
|
|
233
|
-
|
|
234
|
-
<SettingsField label="Button URL">
|
|
235
|
-
<StyledInput
|
|
236
|
-
value={cta.url || ""}
|
|
237
|
-
onFocus={snapshotOnFocus}
|
|
238
|
-
onChange={(v) =>
|
|
239
|
-
updateDebounced({ cta_button: { ...cta, url: v } })
|
|
240
|
-
}
|
|
241
|
-
placeholder="/work/project-name"
|
|
242
|
-
/>
|
|
243
|
-
</SettingsField>
|
|
244
|
-
|
|
245
|
-
<SettingsField label="Style">
|
|
246
|
-
<div className="flex gap-1">
|
|
247
|
-
{(["primary", "secondary", "outline", "text"] as const).map((s) => (
|
|
248
|
-
<button
|
|
249
|
-
key={s}
|
|
250
|
-
onClick={() =>
|
|
251
|
-
update({ cta_button: { ...cta, style: s } })
|
|
252
|
-
}
|
|
253
|
-
className={`flex-1 rounded border py-1 text-[10px] capitalize transition-colors ${
|
|
254
|
-
(cta.style || "primary") === s
|
|
255
|
-
? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
|
|
256
|
-
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
257
|
-
}`}
|
|
258
|
-
>
|
|
259
|
-
{s}
|
|
260
|
-
</button>
|
|
261
|
-
))}
|
|
262
|
-
</div>
|
|
263
|
-
</SettingsField>
|
|
264
|
-
|
|
265
|
-
<StyledCheckbox
|
|
266
|
-
label="Open in new tab"
|
|
267
|
-
checked={cta.target_blank || false}
|
|
268
|
-
onChange={(v) =>
|
|
269
|
-
update({ cta_button: { ...cta, target_blank: v } })
|
|
270
|
-
}
|
|
271
|
-
/>
|
|
272
|
-
</SettingsSection>
|
|
273
|
-
|
|
274
|
-
{/* ========== MEDIA BACKGROUND ========== */}
|
|
275
|
-
<SettingsSection title="Cover Background" defaultOpen icon={<CoverBackgroundIcon />}>
|
|
276
|
-
<SettingsField label="Media Type">
|
|
277
|
-
<div className="flex gap-1">
|
|
278
|
-
{(["image", "video"] as const).map((t) => (
|
|
279
|
-
<button
|
|
280
|
-
key={t}
|
|
281
|
-
onClick={() => update({ media_type: t })}
|
|
282
|
-
className={`flex-1 rounded border py-1.5 text-xs capitalize transition-colors ${
|
|
283
|
-
(block.media_type || "image") === t
|
|
284
|
-
? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
|
|
285
|
-
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
286
|
-
}`}
|
|
287
|
-
>
|
|
288
|
-
{t}
|
|
289
|
-
</button>
|
|
290
|
-
))}
|
|
291
|
-
</div>
|
|
292
|
-
</SettingsField>
|
|
293
|
-
|
|
294
|
-
<SettingsField
|
|
295
|
-
label={block.media_type === "video" ? "Video Path" : "Image Path"}
|
|
296
|
-
>
|
|
297
|
-
<AssetPathInput
|
|
298
|
-
value={block.media_path || ""}
|
|
299
|
-
onFocus={snapshotOnFocus}
|
|
300
|
-
onChange={(v) => updateDebounced({ media_path: v })}
|
|
301
|
-
placeholder={
|
|
302
|
-
block.media_type === "video"
|
|
303
|
-
? "projects/cover.mp4"
|
|
304
|
-
: "projects/cover.jpg"
|
|
305
|
-
}
|
|
306
|
-
filterType={block.media_type === "video" ? "video" : "image"}
|
|
307
|
-
/>
|
|
308
|
-
</SettingsField>
|
|
309
|
-
|
|
310
|
-
{block.media_type === "video" && (
|
|
311
|
-
<SettingsField label="Poster Image" hint="Fallback while video loads">
|
|
312
|
-
<AssetPathInput
|
|
313
|
-
value={block.video_poster || ""}
|
|
314
|
-
onFocus={snapshotOnFocus}
|
|
315
|
-
onChange={(v) => updateDebounced({ video_poster: v })}
|
|
316
|
-
placeholder="projects/cover-poster.jpg"
|
|
317
|
-
filterType="image"
|
|
318
|
-
/>
|
|
319
|
-
</SettingsField>
|
|
320
|
-
)}
|
|
321
|
-
|
|
322
|
-
<SettingsField label="Size">
|
|
323
|
-
<div className="flex gap-1">
|
|
324
|
-
{(["cover", "contain", "none"] as const).map((s) => (
|
|
325
|
-
<button
|
|
326
|
-
key={s}
|
|
327
|
-
onClick={() => update({ background_size: s })}
|
|
328
|
-
className={`flex-1 rounded border py-1 text-[10px] transition-colors ${
|
|
329
|
-
(block.background_size || "cover") === s
|
|
330
|
-
? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
|
|
331
|
-
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
332
|
-
}`}
|
|
333
|
-
>
|
|
334
|
-
{s === "none" ? "No Scale" : s.charAt(0).toUpperCase() + s.slice(1)}
|
|
335
|
-
</button>
|
|
336
|
-
))}
|
|
337
|
-
</div>
|
|
338
|
-
</SettingsField>
|
|
339
|
-
|
|
340
|
-
<SettingsField label="Position">
|
|
341
|
-
<PositionGrid
|
|
342
|
-
value={block.background_position || "center center"}
|
|
343
|
-
onChange={(v) => update({ background_position: v })}
|
|
344
|
-
/>
|
|
345
|
-
</SettingsField>
|
|
346
|
-
|
|
347
|
-
<SettingsField label="Repeat">
|
|
348
|
-
<select
|
|
349
|
-
value={block.background_repeat || "no-repeat"}
|
|
350
|
-
onChange={(e) =>
|
|
351
|
-
update({
|
|
352
|
-
background_repeat: e.target.value as CoverBlock["background_repeat"],
|
|
353
|
-
})
|
|
354
|
-
}
|
|
355
|
-
className={SELECT_CLASS}
|
|
356
|
-
>
|
|
357
|
-
<option value="no-repeat">No Repeat</option>
|
|
358
|
-
<option value="repeat">Repeat</option>
|
|
359
|
-
<option value="repeat-x">Repeat X</option>
|
|
360
|
-
<option value="repeat-y">Repeat Y</option>
|
|
361
|
-
</select>
|
|
362
|
-
</SettingsField>
|
|
363
|
-
</SettingsSection>
|
|
364
|
-
|
|
365
|
-
{/* ========== OVERLAY ========== */}
|
|
366
|
-
<SettingsSection title="Cover Effects" icon={<CoverEffectsIcon />}>
|
|
367
|
-
{/* Toggle: Custom gradient vs Preset overlay */}
|
|
368
|
-
<SettingsField label="Overlay Mode">
|
|
369
|
-
<div className="flex gap-1">
|
|
370
|
-
{(["preset", "custom"] as const).map((mode) => {
|
|
371
|
-
const isActive = mode === "custom" ? !!block.overlay_gradient : !block.overlay_gradient;
|
|
372
|
-
return (
|
|
373
|
-
<button
|
|
374
|
-
key={mode}
|
|
375
|
-
onClick={() => {
|
|
376
|
-
if (mode === "custom" && !block.overlay_gradient) {
|
|
377
|
-
// Switch to custom: initialize with a dark semi-transparent solid
|
|
378
|
-
update({ overlay_gradient: "#00000080" });
|
|
379
|
-
} else if (mode === "preset" && block.overlay_gradient) {
|
|
380
|
-
// Switch to preset: clear overlay_gradient
|
|
381
|
-
update({ overlay_gradient: undefined });
|
|
382
|
-
}
|
|
383
|
-
}}
|
|
384
|
-
className={`flex-1 rounded border py-1 text-[10px] capitalize transition-colors ${
|
|
385
|
-
isActive
|
|
386
|
-
? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
|
|
387
|
-
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
388
|
-
}`}
|
|
389
|
-
>
|
|
390
|
-
{mode === "preset" ? "Preset" : "Custom"}
|
|
391
|
-
</button>
|
|
392
|
-
);
|
|
393
|
-
})}
|
|
394
|
-
</div>
|
|
395
|
-
</SettingsField>
|
|
396
|
-
|
|
397
|
-
{/* Preset overlay controls */}
|
|
398
|
-
{!block.overlay_gradient && (
|
|
399
|
-
<>
|
|
400
|
-
<SettingsField label="Overlay">
|
|
401
|
-
<select
|
|
402
|
-
value={block.overlay || "dark"}
|
|
403
|
-
onChange={(e) =>
|
|
404
|
-
update({
|
|
405
|
-
overlay: e.target.value as CoverBlock["overlay"],
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
className={SELECT_CLASS}
|
|
409
|
-
>
|
|
410
|
-
<option value="none">None</option>
|
|
411
|
-
<option value="dark">Dark</option>
|
|
412
|
-
<option value="light">Light</option>
|
|
413
|
-
<option value="gradient-bottom">Gradient (Bottom)</option>
|
|
414
|
-
<option value="gradient-top">Gradient (Top)</option>
|
|
415
|
-
</select>
|
|
416
|
-
</SettingsField>
|
|
417
|
-
|
|
418
|
-
{block.overlay && block.overlay !== "none" && (
|
|
419
|
-
<SettingsField label="Opacity">
|
|
420
|
-
<div className="flex items-center gap-2">
|
|
421
|
-
<input
|
|
422
|
-
type="range"
|
|
423
|
-
min={0}
|
|
424
|
-
max={100}
|
|
425
|
-
value={block.overlay_opacity ?? 50}
|
|
426
|
-
onChange={(e) =>
|
|
427
|
-
update({ overlay_opacity: parseInt(e.target.value, 10) })
|
|
428
|
-
}
|
|
429
|
-
className="flex-1 accent-[#076bff]"
|
|
430
|
-
/>
|
|
431
|
-
<span className="text-xs text-neutral-500 w-8 text-right">
|
|
432
|
-
{block.overlay_opacity ?? 50}%
|
|
433
|
-
</span>
|
|
434
|
-
</div>
|
|
435
|
-
</SettingsField>
|
|
436
|
-
)}
|
|
437
|
-
</>
|
|
438
|
-
)}
|
|
439
|
-
|
|
440
|
-
{/* Custom overlay gradient (Phase 4) */}
|
|
441
|
-
{block.overlay_gradient && (
|
|
442
|
-
<SettingsField label="Overlay Color">
|
|
443
|
-
<ColorSwatchPicker
|
|
444
|
-
value={(() => {
|
|
445
|
-
const parsed = parseColorField(block.overlay_gradient);
|
|
446
|
-
return typeof parsed === "string" ? parsed : parsed;
|
|
447
|
-
})()}
|
|
448
|
-
onChange={(val: ColorField) => {
|
|
449
|
-
update({ overlay_gradient: serializeColorField(val) });
|
|
450
|
-
}}
|
|
451
|
-
swatches={paletteSwatches}
|
|
452
|
-
allowGradients
|
|
453
|
-
/>
|
|
454
|
-
</SettingsField>
|
|
455
|
-
)}
|
|
456
|
-
</SettingsSection>
|
|
457
|
-
|
|
458
|
-
{/* ========== LAYOUT ========== */}
|
|
459
|
-
<SettingsSection title="Layout" icon={<LayoutIcon />}>
|
|
460
|
-
<ResponsiveField
|
|
461
|
-
label="Content Pos"
|
|
462
|
-
block={block as ContentBlock}
|
|
463
|
-
property="content_align_h"
|
|
464
|
-
onReset={() => {
|
|
465
|
-
resetOverride("content_align_h");
|
|
466
|
-
resetOverride("content_align_v");
|
|
467
|
-
}}
|
|
468
|
-
>
|
|
469
|
-
<ContentAlignGrid
|
|
470
|
-
alignH={effectiveAlignH}
|
|
471
|
-
alignV={effectiveAlignV}
|
|
472
|
-
onChangeH={(v) => updateResponsive("content_align_h", v)}
|
|
473
|
-
onChangeV={(v) => updateResponsive("content_align_v", v)}
|
|
474
|
-
/>
|
|
475
|
-
</ResponsiveField>
|
|
476
|
-
|
|
477
|
-
<ResponsiveField
|
|
478
|
-
label="Max Width"
|
|
479
|
-
block={block as ContentBlock}
|
|
480
|
-
property="content_max_width"
|
|
481
|
-
hint="Text container width"
|
|
482
|
-
onReset={() => resetOverride("content_max_width")}
|
|
483
|
-
>
|
|
484
|
-
<StyledInput
|
|
485
|
-
value={effectiveMaxWidth}
|
|
486
|
-
onFocus={snapshotOnFocus}
|
|
487
|
-
onChange={(v) => updateResponsiveDebounced("content_max_width", v)}
|
|
488
|
-
placeholder="800px"
|
|
489
|
-
/>
|
|
490
|
-
</ResponsiveField>
|
|
491
|
-
|
|
492
|
-
<ResponsiveField
|
|
493
|
-
label="Height"
|
|
494
|
-
block={block as ContentBlock}
|
|
495
|
-
property="height"
|
|
496
|
-
onReset={() => resetOverride("height")}
|
|
497
|
-
>
|
|
498
|
-
<select
|
|
499
|
-
value={effectiveHeight}
|
|
500
|
-
onChange={(e) =>
|
|
501
|
-
updateResponsive("height", e.target.value)
|
|
502
|
-
}
|
|
503
|
-
className={SELECT_CLASS}
|
|
504
|
-
>
|
|
505
|
-
<option value="100vh">Full Screen (100vh)</option>
|
|
506
|
-
<option value="80vh">Large (80vh)</option>
|
|
507
|
-
<option value="60vh">Medium (60vh)</option>
|
|
508
|
-
<option value="40vh">Small (40vh)</option>
|
|
509
|
-
<option value="custom">Custom</option>
|
|
510
|
-
</select>
|
|
511
|
-
</ResponsiveField>
|
|
512
|
-
|
|
513
|
-
{effectiveHeight === "custom" && (
|
|
514
|
-
<ResponsiveField
|
|
515
|
-
label="Custom Height"
|
|
516
|
-
block={block as ContentBlock}
|
|
517
|
-
property="custom_height"
|
|
518
|
-
onReset={() => resetOverride("custom_height")}
|
|
519
|
-
>
|
|
520
|
-
<StyledInput
|
|
521
|
-
value={effectiveCustomHeight}
|
|
522
|
-
onFocus={snapshotOnFocus}
|
|
523
|
-
onChange={(v) => updateResponsiveDebounced("custom_height", v)}
|
|
524
|
-
placeholder="500px, 70vh"
|
|
525
|
-
/>
|
|
526
|
-
</ResponsiveField>
|
|
527
|
-
)}
|
|
528
|
-
|
|
529
|
-
{/* Legacy mobile_height removed — use tablet/phone viewport overrides instead */}
|
|
530
|
-
</SettingsSection>
|
|
531
|
-
|
|
532
|
-
{/* ========== APPEARANCE ========== */}
|
|
533
|
-
<SettingsSection title="Appearance" icon={<AppearanceIcon />}>
|
|
534
|
-
<SettingsField label="Text Color">
|
|
535
|
-
<ColorSwatchPicker
|
|
536
|
-
value={block.text_color || ""}
|
|
537
|
-
onChange={(hex) => update({ text_color: (typeof hex === "string" ? hex : "#ffffff") || "#ffffff" })}
|
|
538
|
-
swatches={paletteSwatches}
|
|
539
|
-
/>
|
|
540
|
-
</SettingsField>
|
|
541
|
-
|
|
542
|
-
<StyledCheckbox
|
|
543
|
-
label="Show scroll down arrow"
|
|
544
|
-
checked={block.show_scroll_indicator || false}
|
|
545
|
-
onChange={(v) => update({ show_scroll_indicator: v })}
|
|
546
|
-
/>
|
|
547
|
-
</SettingsSection>
|
|
548
|
-
</>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { adminAssetUrl } from "../../../lib/assets";
|
|
4
|
-
import { ThumbBadge } from "./shared";
|
|
5
|
-
import type { CoverBlock } from "../../../lib/sanity/types";
|
|
6
|
-
|
|
7
|
-
export default function LiveCoverPreview({ block }: { block: CoverBlock }) {
|
|
8
|
-
const mediaSrc = block.media_path ? adminAssetUrl(block.media_path) : undefined;
|
|
9
|
-
const posterSrc = block.video_poster ? adminAssetUrl(block.video_poster) : undefined;
|
|
10
|
-
const isVideo = block.media_type === "video";
|
|
11
|
-
|
|
12
|
-
const overlayOpacity = (block.overlay_opacity ?? 50) / 100;
|
|
13
|
-
let overlayBg = "";
|
|
14
|
-
switch (block.overlay) {
|
|
15
|
-
case "dark":
|
|
16
|
-
overlayBg = `rgba(0,0,0,${overlayOpacity})`;
|
|
17
|
-
break;
|
|
18
|
-
case "light":
|
|
19
|
-
overlayBg = `rgba(255,255,255,${overlayOpacity})`;
|
|
20
|
-
break;
|
|
21
|
-
case "gradient-bottom":
|
|
22
|
-
overlayBg = `linear-gradient(to top, rgba(0,0,0,${overlayOpacity}) 0%, transparent 60%)`;
|
|
23
|
-
break;
|
|
24
|
-
case "gradient-top":
|
|
25
|
-
overlayBg = `linear-gradient(to bottom, rgba(0,0,0,${overlayOpacity}) 0%, transparent 60%)`;
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const alignMap: Record<string, string> = {
|
|
30
|
-
top: "flex-start",
|
|
31
|
-
center: "center",
|
|
32
|
-
bottom: "flex-end",
|
|
33
|
-
left: "flex-start",
|
|
34
|
-
right: "flex-end",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const textColor = block.text_color || "#ffffff";
|
|
38
|
-
|
|
39
|
-
// Resolve height — must match CoverBlockRenderer on public site
|
|
40
|
-
const height =
|
|
41
|
-
block.height === "custom" && block.custom_height
|
|
42
|
-
? block.custom_height
|
|
43
|
-
: block.height || "100vh";
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div
|
|
47
|
-
className="relative rounded overflow-hidden"
|
|
48
|
-
style={{
|
|
49
|
-
height,
|
|
50
|
-
minHeight: height,
|
|
51
|
-
display: "flex",
|
|
52
|
-
alignItems: alignMap[block.content_align_v || "center"],
|
|
53
|
-
justifyContent: alignMap[block.content_align_h || "center"],
|
|
54
|
-
color: textColor,
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{/* Background */}
|
|
58
|
-
{mediaSrc && !isVideo && (
|
|
59
|
-
<>
|
|
60
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
61
|
-
<img
|
|
62
|
-
src={mediaSrc}
|
|
63
|
-
alt=""
|
|
64
|
-
className="absolute inset-0 w-full h-full"
|
|
65
|
-
style={{
|
|
66
|
-
objectFit: (block.background_size || "cover") as "cover" | "contain" | "none",
|
|
67
|
-
objectPosition: block.background_position || "center center",
|
|
68
|
-
}}
|
|
69
|
-
/>
|
|
70
|
-
{block.media_path && <ThumbBadge assetPath={block.media_path} />}
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
{mediaSrc && isVideo && (
|
|
74
|
-
<video
|
|
75
|
-
src={mediaSrc}
|
|
76
|
-
poster={posterSrc}
|
|
77
|
-
autoPlay
|
|
78
|
-
loop
|
|
79
|
-
muted
|
|
80
|
-
playsInline
|
|
81
|
-
className="absolute inset-0 w-full h-full"
|
|
82
|
-
style={{
|
|
83
|
-
objectFit: (block.background_size || "cover") as "cover" | "contain" | "none",
|
|
84
|
-
objectPosition: block.background_position || "center center",
|
|
85
|
-
}}
|
|
86
|
-
/>
|
|
87
|
-
)}
|
|
88
|
-
{!mediaSrc && posterSrc && (
|
|
89
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
90
|
-
<img
|
|
91
|
-
src={posterSrc}
|
|
92
|
-
alt=""
|
|
93
|
-
className="absolute inset-0 w-full h-full object-cover"
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
96
|
-
{!mediaSrc && !posterSrc && <div className="absolute inset-0 bg-neutral-900" />}
|
|
97
|
-
|
|
98
|
-
{/* Overlay */}
|
|
99
|
-
{overlayBg && (
|
|
100
|
-
<div
|
|
101
|
-
className="absolute inset-0"
|
|
102
|
-
style={{ background: overlayBg }}
|
|
103
|
-
/>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{/* Content */}
|
|
107
|
-
<div
|
|
108
|
-
className="relative z-10 flex flex-col gap-3 py-12"
|
|
109
|
-
style={{
|
|
110
|
-
maxWidth: block.content_max_width || "800px",
|
|
111
|
-
textAlign: (block.content_align_h || "center") as "left" | "center" | "right",
|
|
112
|
-
width: "100%",
|
|
113
|
-
paddingLeft: "var(--grid-padding, 24px)",
|
|
114
|
-
paddingRight: "var(--grid-padding, 24px)",
|
|
115
|
-
}}
|
|
116
|
-
>
|
|
117
|
-
{block.headline && (
|
|
118
|
-
<h1 className="text-2xl font-bold uppercase tracking-widest">
|
|
119
|
-
{block.headline}
|
|
120
|
-
</h1>
|
|
121
|
-
)}
|
|
122
|
-
{block.subheadline && (
|
|
123
|
-
<p className="text-xs opacity-80 uppercase tracking-wider">
|
|
124
|
-
{block.subheadline}
|
|
125
|
-
</p>
|
|
126
|
-
)}
|
|
127
|
-
{block.cta_button?.text && (
|
|
128
|
-
<div>
|
|
129
|
-
<span className="inline-block rounded-lg bg-white text-black text-xs px-6 py-3">
|
|
130
|
-
{block.cta_button.text}
|
|
131
|
-
</span>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{/* Scroll indicator */}
|
|
137
|
-
{block.show_scroll_indicator && (
|
|
138
|
-
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 opacity-60">
|
|
139
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
140
|
-
<path d="M12 5v14M5 12l7 7 7-7" />
|
|
141
|
-
</svg>
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|