@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,261 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { CoverBlock } from "../../lib/sanity/types";
|
|
4
|
-
import { useAssetUrl } from "../../lib/contexts/AssetContext";
|
|
5
|
-
import { handleImageRetry, handleVideoRetry } from "../../lib/asset-retry";
|
|
6
|
-
import { parseColorField, colorToCSS, isGradient } from "../../lib/color-utils";
|
|
7
|
-
|
|
8
|
-
function getOverlayStyle(
|
|
9
|
-
overlay: CoverBlock["overlay"],
|
|
10
|
-
opacity: number
|
|
11
|
-
): React.CSSProperties | null {
|
|
12
|
-
const alpha = opacity / 100;
|
|
13
|
-
switch (overlay) {
|
|
14
|
-
case "dark":
|
|
15
|
-
return { backgroundColor: `rgba(0,0,0,${alpha})` };
|
|
16
|
-
case "light":
|
|
17
|
-
return { backgroundColor: `rgba(255,255,255,${alpha})` };
|
|
18
|
-
case "gradient-bottom":
|
|
19
|
-
return {
|
|
20
|
-
background: `linear-gradient(to top, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
|
|
21
|
-
};
|
|
22
|
-
case "gradient-top":
|
|
23
|
-
return {
|
|
24
|
-
background: `linear-gradient(to bottom, rgba(0,0,0,${alpha}) 0%, transparent 60%)`,
|
|
25
|
-
};
|
|
26
|
-
default:
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getAlignItems(v: CoverBlock["content_align_v"]): string {
|
|
32
|
-
switch (v) {
|
|
33
|
-
case "top":
|
|
34
|
-
return "flex-start";
|
|
35
|
-
case "bottom":
|
|
36
|
-
return "flex-end";
|
|
37
|
-
default:
|
|
38
|
-
return "center";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getJustify(h: CoverBlock["content_align_h"]): string {
|
|
43
|
-
switch (h) {
|
|
44
|
-
case "left":
|
|
45
|
-
return "flex-start";
|
|
46
|
-
case "right":
|
|
47
|
-
return "flex-end";
|
|
48
|
-
default:
|
|
49
|
-
return "center";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getTextAlign(
|
|
54
|
-
h: CoverBlock["content_align_h"]
|
|
55
|
-
): "left" | "center" | "right" {
|
|
56
|
-
return h || "center";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export default function CoverBlockRenderer({
|
|
60
|
-
block,
|
|
61
|
-
}: {
|
|
62
|
-
block: CoverBlock;
|
|
63
|
-
}) {
|
|
64
|
-
const height =
|
|
65
|
-
block.height === "custom" && block.custom_height
|
|
66
|
-
? block.custom_height
|
|
67
|
-
: block.height || "100vh";
|
|
68
|
-
|
|
69
|
-
const mobileHeight =
|
|
70
|
-
block.mobile_height && block.mobile_height !== "same"
|
|
71
|
-
? block.mobile_height
|
|
72
|
-
: null;
|
|
73
|
-
|
|
74
|
-
// Phase 4: custom overlay_gradient takes precedence over hardcoded presets
|
|
75
|
-
const overlayStyle: React.CSSProperties | null = (() => {
|
|
76
|
-
if (block.overlay_gradient) {
|
|
77
|
-
const parsed = parseColorField(block.overlay_gradient);
|
|
78
|
-
if (isGradient(parsed)) {
|
|
79
|
-
return { backgroundImage: colorToCSS(parsed) };
|
|
80
|
-
}
|
|
81
|
-
// Solid color overlay from gradient field
|
|
82
|
-
return { backgroundColor: colorToCSS(parsed) };
|
|
83
|
-
}
|
|
84
|
-
return getOverlayStyle(block.overlay ?? "none", block.overlay_opacity ?? 50);
|
|
85
|
-
})();
|
|
86
|
-
|
|
87
|
-
const resolveAsset = useAssetUrl();
|
|
88
|
-
const mediaSrc = block.media_path ? resolveAsset(block.media_path) : undefined;
|
|
89
|
-
const posterSrc = block.video_poster
|
|
90
|
-
? resolveAsset(block.video_poster)
|
|
91
|
-
: undefined;
|
|
92
|
-
const isVideo = block.media_type === "video";
|
|
93
|
-
|
|
94
|
-
// BLK-010: Validate objectFit against allowed CSS values
|
|
95
|
-
const allowedObjectFit = new Set(["cover", "contain", "none", "fill", "scale-down"]);
|
|
96
|
-
const objectFit = allowedObjectFit.has(block.background_size || "") ? block.background_size! : "cover";
|
|
97
|
-
const objectPosition = block.background_position || "center center";
|
|
98
|
-
|
|
99
|
-
// Guard: text color must be a solid hex string. Fallback if gradient slips through.
|
|
100
|
-
const rawTextColor = block.text_color || "#ffffff";
|
|
101
|
-
const textColor = typeof rawTextColor === "string" ? rawTextColor : "#ffffff";
|
|
102
|
-
|
|
103
|
-
const ctaStyleClasses: Record<string, string> = {
|
|
104
|
-
primary:
|
|
105
|
-
"bg-brand-accent-alt text-brand-dark hover:bg-brand-accent",
|
|
106
|
-
secondary:
|
|
107
|
-
"bg-brand-dark text-white hover:bg-neutral-800",
|
|
108
|
-
outline:
|
|
109
|
-
"border-2 border-current bg-transparent hover:bg-white/10",
|
|
110
|
-
text: "underline underline-offset-4 hover:opacity-70",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// BLK-002: Sanitize _key and mobileHeight before CSS interpolation
|
|
114
|
-
// _key: allow only alphanumeric, hyphens, underscores (strip anything else)
|
|
115
|
-
const safeKey = block._key?.replace(/[^a-zA-Z0-9_-]/g, "") || "";
|
|
116
|
-
// mobileHeight: must match valid CSS height (number+unit or CSS keyword)
|
|
117
|
-
const safeMobileHeight =
|
|
118
|
-
mobileHeight && /^(\d+(\.\d+)?(px|vh|vw|em|rem|%|svh|dvh)|auto|inherit)$/i.test(mobileHeight)
|
|
119
|
-
? mobileHeight
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<>
|
|
124
|
-
{/* Mobile height override via inline style tag */}
|
|
125
|
-
{safeMobileHeight && safeKey && (
|
|
126
|
-
<style
|
|
127
|
-
dangerouslySetInnerHTML={{
|
|
128
|
-
__html: `@media(max-width:767px){.cover-block-${safeKey}{height:${safeMobileHeight}!important;min-height:${safeMobileHeight}!important;}}`,
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
|
|
133
|
-
<section
|
|
134
|
-
className={`cover-block-${safeKey} relative flex overflow-hidden`}
|
|
135
|
-
style={{
|
|
136
|
-
height,
|
|
137
|
-
minHeight: height,
|
|
138
|
-
alignItems: getAlignItems(block.content_align_v),
|
|
139
|
-
justifyContent: getJustify(block.content_align_h),
|
|
140
|
-
color: textColor,
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{/* Media layer — uses <img> instead of background-image for error recovery */}
|
|
144
|
-
{mediaSrc && !isVideo && (
|
|
145
|
-
/* eslint-disable-next-line @next/next/no-img-element */
|
|
146
|
-
<img
|
|
147
|
-
src={mediaSrc}
|
|
148
|
-
alt={block.headline || ""}
|
|
149
|
-
onError={handleImageRetry}
|
|
150
|
-
className="absolute inset-0 h-full w-full"
|
|
151
|
-
style={{
|
|
152
|
-
objectFit: objectFit as React.CSSProperties["objectFit"],
|
|
153
|
-
objectPosition,
|
|
154
|
-
}}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
|
|
158
|
-
{mediaSrc && isVideo && (
|
|
159
|
-
<video
|
|
160
|
-
src={mediaSrc}
|
|
161
|
-
poster={posterSrc}
|
|
162
|
-
autoPlay
|
|
163
|
-
loop
|
|
164
|
-
muted
|
|
165
|
-
playsInline
|
|
166
|
-
onError={handleVideoRetry}
|
|
167
|
-
className="absolute inset-0 h-full w-full"
|
|
168
|
-
style={{
|
|
169
|
-
objectFit: objectFit as React.CSSProperties["objectFit"],
|
|
170
|
-
objectPosition,
|
|
171
|
-
}}
|
|
172
|
-
/>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
{/* Fallback: poster only if no media_path but video_poster exists */}
|
|
176
|
-
{!mediaSrc && posterSrc && (
|
|
177
|
-
/* eslint-disable-next-line @next/next/no-img-element */
|
|
178
|
-
<img
|
|
179
|
-
src={posterSrc}
|
|
180
|
-
alt=""
|
|
181
|
-
onError={handleImageRetry}
|
|
182
|
-
className="absolute inset-0 h-full w-full"
|
|
183
|
-
style={{ objectFit: "cover", objectPosition: "center" }}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
|
|
187
|
-
{/* No media fallback */}
|
|
188
|
-
{!mediaSrc && !posterSrc && (
|
|
189
|
-
<div className="absolute inset-0 bg-brand-dark" />
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Overlay */}
|
|
193
|
-
{overlayStyle && <div className="absolute inset-0" style={overlayStyle} />}
|
|
194
|
-
|
|
195
|
-
{/* Content */}
|
|
196
|
-
<div
|
|
197
|
-
className="relative z-10 flex flex-col gap-4 py-12"
|
|
198
|
-
style={{
|
|
199
|
-
maxWidth: block.content_max_width || "800px",
|
|
200
|
-
textAlign: getTextAlign(block.content_align_h),
|
|
201
|
-
width: "100%",
|
|
202
|
-
paddingLeft: "var(--grid-padding, 24px)",
|
|
203
|
-
paddingRight: "var(--grid-padding, 24px)",
|
|
204
|
-
}}
|
|
205
|
-
>
|
|
206
|
-
{block.headline && (
|
|
207
|
-
<h1 className="font-sans text-4xl uppercase tracking-widest md:text-6xl lg:text-7xl">
|
|
208
|
-
{block.headline}
|
|
209
|
-
</h1>
|
|
210
|
-
)}
|
|
211
|
-
|
|
212
|
-
{block.subheadline && (
|
|
213
|
-
<p className="font-sans text-sm uppercase tracking-wider opacity-80 md:text-base">
|
|
214
|
-
{block.subheadline}
|
|
215
|
-
</p>
|
|
216
|
-
)}
|
|
217
|
-
|
|
218
|
-
{block.cta_button?.text && block.cta_button.url && (
|
|
219
|
-
<div>
|
|
220
|
-
<a
|
|
221
|
-
href={block.cta_button.url}
|
|
222
|
-
target={
|
|
223
|
-
block.cta_button.target_blank ? "_blank" : undefined
|
|
224
|
-
}
|
|
225
|
-
rel={
|
|
226
|
-
block.cta_button.target_blank
|
|
227
|
-
? "noopener noreferrer"
|
|
228
|
-
: undefined
|
|
229
|
-
}
|
|
230
|
-
className={`inline-block px-6 py-3 font-sans text-sm uppercase tracking-wider transition ${
|
|
231
|
-
ctaStyleClasses[block.cta_button.style || "primary"]
|
|
232
|
-
}`}
|
|
233
|
-
>
|
|
234
|
-
{block.cta_button.text}
|
|
235
|
-
</a>
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
|
|
240
|
-
{/* Scroll indicator */}
|
|
241
|
-
{block.show_scroll_indicator && (
|
|
242
|
-
<div className="absolute bottom-6 left-1/2 z-10 -translate-x-1/2 animate-bounce">
|
|
243
|
-
<svg
|
|
244
|
-
width="24"
|
|
245
|
-
height="24"
|
|
246
|
-
viewBox="0 0 24 24"
|
|
247
|
-
fill="none"
|
|
248
|
-
stroke="currentColor"
|
|
249
|
-
strokeWidth="2"
|
|
250
|
-
strokeLinecap="round"
|
|
251
|
-
strokeLinejoin="round"
|
|
252
|
-
aria-hidden="true"
|
|
253
|
-
>
|
|
254
|
-
<path d="M12 5v14M5 12l7 7 7-7" />
|
|
255
|
-
</svg>
|
|
256
|
-
</div>
|
|
257
|
-
)}
|
|
258
|
-
</section>
|
|
259
|
-
</>
|
|
260
|
-
);
|
|
261
|
-
}
|