@morphika/andami 0.2.13 → 0.2.15
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/components/blocks/CoverSectionRenderer.tsx +19 -3
- package/components/blocks/ImageBlockRenderer.tsx +12 -10
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/blocks/VideoBlockRenderer.tsx +11 -6
- package/components/builder/SectionV2Column.tsx +1 -0
- package/components/builder/SettingsPanel.tsx +3 -0
- package/components/builder/SortableBlock.tsx +26 -11
- package/components/builder/editors/ImageBlockEditor.tsx +1 -0
- package/components/builder/editors/VideoBlockEditor.tsx +1 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
- package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -0
- package/lib/sanity/queries.ts +18 -4
- package/lib/sanity/types.ts +12 -3
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/imageBlock.ts +1 -0
- package/sanity/schemas/blocks/videoBlock.ts +1 -0
- package/sanity/schemas/objects/coverSection.ts +11 -0
|
@@ -127,12 +127,19 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
127
127
|
rowAlignMap[String(i + 1)] = row.vertical_align || "start";
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
const borderRadius = s.border_radius ? `${String(s.border_radius).replace(/[a-z%]+$/i, "")}px` : undefined;
|
|
131
|
+
|
|
130
132
|
const sectionContent = (
|
|
131
133
|
<section
|
|
132
134
|
style={{
|
|
133
135
|
position: "relative",
|
|
134
136
|
height: section.height,
|
|
135
137
|
overflow: "hidden",
|
|
138
|
+
marginTop: s.offset_top ? `${s.offset_top}px` : undefined,
|
|
139
|
+
marginRight: s.offset_right ? `${s.offset_right}px` : undefined,
|
|
140
|
+
marginBottom: s.offset_bottom ? `${s.offset_bottom}px` : undefined,
|
|
141
|
+
marginLeft: s.offset_left ? `${s.offset_left}px` : undefined,
|
|
142
|
+
borderRadius,
|
|
136
143
|
}}
|
|
137
144
|
>
|
|
138
145
|
{responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
|
|
@@ -221,10 +228,13 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
221
228
|
style={{
|
|
222
229
|
gridColumn: `${col.grid_column} / span ${col.span}`,
|
|
223
230
|
gridRow: col.grid_row,
|
|
224
|
-
|
|
231
|
+
position: "relative",
|
|
232
|
+
display: "flex",
|
|
233
|
+
flexDirection: "column",
|
|
234
|
+
justifyContent: alignSelf === "center" ? "center" : alignSelf === "end" ? "flex-end" : colJustify || "flex-start",
|
|
235
|
+
height: "100%",
|
|
225
236
|
minWidth: 0,
|
|
226
237
|
overflow: "hidden",
|
|
227
|
-
...(colJustify ? { display: "flex", flexDirection: "column" as const, justifyContent: colJustify } : {}),
|
|
228
238
|
}}
|
|
229
239
|
>
|
|
230
240
|
{(col.blocks || []).map((block) => {
|
|
@@ -237,9 +247,15 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
237
247
|
|
|
238
248
|
const layout = (block as ContentBlock & { layout?: BlockLayout }).layout;
|
|
239
249
|
const alignStyles = layout && hasBlockAlignment(layout) ? getBlockAlignmentStyles(layout) : {};
|
|
250
|
+
const isFillBlock = (block._type === "imageBlock" || block._type === "videoBlock") &&
|
|
251
|
+
(block as unknown as { width?: string }).width === "fill";
|
|
240
252
|
|
|
241
253
|
const rendered = (
|
|
242
|
-
<div key={block._key} style={
|
|
254
|
+
<div key={block._key} style={
|
|
255
|
+
isFillBlock
|
|
256
|
+
? { position: "absolute" as const, inset: 0, zIndex: 0 }
|
|
257
|
+
: { position: "relative" as const, zIndex: 1, ...alignStyles }
|
|
258
|
+
}>
|
|
243
259
|
<BlockRenderer block={block} />
|
|
244
260
|
</div>
|
|
245
261
|
);
|
|
@@ -25,24 +25,26 @@ const aspectMap: Record<string, string | undefined> = {
|
|
|
25
25
|
export default function ImageBlockRenderer({ block }: { block: ImageBlock }) {
|
|
26
26
|
const resolveAsset = useAssetUrl();
|
|
27
27
|
const src = resolveAsset(block.asset_path);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const isFill = block.width === "fill";
|
|
29
|
+
const widthStyle = isFill ? {} : (widthStyleMap[block.width ?? "full"] || widthStyleMap.full);
|
|
30
|
+
const aspect = isFill ? undefined : aspectMap[block.aspect_ratio ?? "auto"];
|
|
30
31
|
|
|
31
32
|
// BLK-014: Strip any existing unit suffix, then validate as a number before appending px
|
|
32
33
|
const rawRadius = block.border_radius ? String(block.border_radius).replace(/[a-z%]+$/i, "") : "";
|
|
33
34
|
const borderRadius = rawRadius && !isNaN(Number(rawRadius)) ? `${rawRadius}px` : undefined;
|
|
34
35
|
|
|
35
|
-
const imgStyle: React.CSSProperties =
|
|
36
|
-
width: "100%",
|
|
37
|
-
display: "block",
|
|
38
|
-
objectFit: aspect ? "cover" : undefined,
|
|
39
|
-
aspectRatio: aspect,
|
|
40
|
-
};
|
|
36
|
+
const imgStyle: React.CSSProperties = isFill
|
|
37
|
+
? { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }
|
|
38
|
+
: { width: "100%", display: "block", objectFit: aspect ? "cover" : undefined, aspectRatio: aspect };
|
|
41
39
|
|
|
42
40
|
const imgClassName = block.shadow ? "shadow-lg" : "";
|
|
43
41
|
|
|
42
|
+
const figureStyle: React.CSSProperties = isFill
|
|
43
|
+
? { position: "absolute", inset: 0, borderRadius, overflow: "hidden" }
|
|
44
|
+
: { ...widthStyle, borderRadius, overflow: "hidden" };
|
|
45
|
+
|
|
44
46
|
return (
|
|
45
|
-
<figure style={
|
|
47
|
+
<figure style={figureStyle}>
|
|
46
48
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
47
49
|
<img
|
|
48
50
|
src={src}
|
|
@@ -53,7 +55,7 @@ export default function ImageBlockRenderer({ block }: { block: ImageBlock }) {
|
|
|
53
55
|
style={imgStyle}
|
|
54
56
|
className={imgClassName}
|
|
55
57
|
/>
|
|
56
|
-
{block.caption && (
|
|
58
|
+
{!isFill && block.caption && (
|
|
57
59
|
<figcaption className="mt-2 font-sans text-xs uppercase tracking-wider text-brand-muted">
|
|
58
60
|
{block.caption}
|
|
59
61
|
</figcaption>
|
|
@@ -268,6 +268,7 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
268
268
|
style={{
|
|
269
269
|
gridColumn: `${col.grid_column} / span ${col.span}`,
|
|
270
270
|
gridRow: col.grid_row,
|
|
271
|
+
position: "relative",
|
|
271
272
|
display: "flex",
|
|
272
273
|
flexDirection: "column",
|
|
273
274
|
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
@@ -280,8 +281,14 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
280
281
|
const blockLayout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
|
|
281
282
|
const alignStyles = hasBlockAlignment(blockLayout) ? getBlockAlignmentStyles(blockLayout) : undefined;
|
|
282
283
|
const hasHAlign = blockLayout?.align_h && blockLayout.align_h !== "left";
|
|
284
|
+
const isFillBlock = (block._type === "imageBlock" || block._type === "videoBlock") &&
|
|
285
|
+
(block as unknown as { width?: string }).width === "fill";
|
|
283
286
|
return (
|
|
284
|
-
<div key={block._key} className={`blk-wrap-${block._key}`} style={
|
|
287
|
+
<div key={block._key} className={`blk-wrap-${block._key}`} style={
|
|
288
|
+
isFillBlock
|
|
289
|
+
? { position: "absolute" as const, inset: 0, zIndex: 0 }
|
|
290
|
+
: { ...(!hasHAlign ? { width: "100%" } : { width: "auto", maxWidth: "100%" }), minWidth: 0, position: "relative" as const, zIndex: 1, ...alignStyles }
|
|
291
|
+
}>
|
|
285
292
|
<BlockRenderer
|
|
286
293
|
block={block}
|
|
287
294
|
columnEnterAnimation={col.enter_animation}
|
|
@@ -286,18 +286,23 @@ function NativeVideo({ block, paddingBottom, resolveAsset }: {
|
|
|
286
286
|
|
|
287
287
|
export default function VideoBlockRenderer({ block }: { block: VideoBlock }) {
|
|
288
288
|
const resolveAsset = useAssetUrl();
|
|
289
|
-
const
|
|
290
|
-
const
|
|
289
|
+
const isFill = block.width === "fill";
|
|
290
|
+
const widthStyle = isFill ? {} : (widthStyleMap[block.width ?? "full"] || widthStyleMap.full);
|
|
291
|
+
const paddingBottom = isFill ? "100%" : (aspectMap[block.aspect_ratio ?? "16:9"] || "56.25%");
|
|
291
292
|
const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
|
|
292
293
|
|
|
294
|
+
const containerStyle: React.CSSProperties = isFill
|
|
295
|
+
? { position: "absolute", inset: 0, borderRadius, overflow: "hidden" }
|
|
296
|
+
: { ...widthStyle, borderRadius, overflow: borderRadius ? "hidden" : undefined };
|
|
297
|
+
|
|
293
298
|
return (
|
|
294
|
-
<div style={
|
|
299
|
+
<div style={containerStyle}>
|
|
295
300
|
{block.video_type === "vimeo" ? (
|
|
296
|
-
<VimeoEmbed block={block} paddingBottom={paddingBottom} />
|
|
301
|
+
<VimeoEmbed block={block} paddingBottom={isFill ? "100%" : paddingBottom} />
|
|
297
302
|
) : block.video_type === "youtube" ? (
|
|
298
|
-
<YouTubeEmbed block={block} paddingBottom={paddingBottom} />
|
|
303
|
+
<YouTubeEmbed block={block} paddingBottom={isFill ? "100%" : paddingBottom} />
|
|
299
304
|
) : (
|
|
300
|
-
<NativeVideo block={block} paddingBottom={paddingBottom} resolveAsset={resolveAsset} />
|
|
305
|
+
<NativeVideo block={block} paddingBottom={isFill ? "100%" : paddingBottom} resolveAsset={resolveAsset} />
|
|
301
306
|
)}
|
|
302
307
|
</div>
|
|
303
308
|
);
|
|
@@ -233,6 +233,7 @@ export default function SectionV2Column({
|
|
|
233
233
|
style={{
|
|
234
234
|
gridColumn: `${column.grid_column} / span ${column.span}`,
|
|
235
235
|
gridRow: column.grid_row,
|
|
236
|
+
position: "relative",
|
|
236
237
|
display: "flex",
|
|
237
238
|
flexDirection: "column",
|
|
238
239
|
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
ParallaxGroupSettings,
|
|
40
40
|
CoverSectionSettings,
|
|
41
41
|
} from "./settings-panel";
|
|
42
|
+
import CoverSectionLayoutTab from "./settings-panel/CoverSectionLayoutTab";
|
|
42
43
|
|
|
43
44
|
type SettingsTab = "settings" | "layout" | "seo" | "animation";
|
|
44
45
|
|
|
@@ -337,6 +338,8 @@ export default function SettingsPanel() {
|
|
|
337
338
|
isCoverSectionOnly && selectedCoverSection ? (
|
|
338
339
|
activeTab === "animation" ? (
|
|
339
340
|
<SectionV2AnimationTab section={effectiveSectionV2!} />
|
|
341
|
+
) : activeTab === "layout" ? (
|
|
342
|
+
<CoverSectionLayoutTab section={selectedCoverSection} />
|
|
340
343
|
) : (
|
|
341
344
|
<CoverSectionSettings section={selectedCoverSection} />
|
|
342
345
|
)
|
|
@@ -7,7 +7,7 @@ import { makeBlockId } from "./DndWrapper";
|
|
|
7
7
|
import { ALL_BLOCK_INFO, isSectionBlockType } from "../../lib/builder/types";
|
|
8
8
|
import type { DeviceViewport } from "../../lib/builder/types";
|
|
9
9
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
10
|
-
import type { ContentBlock } from "../../lib/sanity/types";
|
|
10
|
+
import type { ContentBlock, ImageBlock, VideoBlock } from "../../lib/sanity/types";
|
|
11
11
|
import BlockLivePreview from "./BlockLivePreview";
|
|
12
12
|
import { getBlockAlignmentStyles, hasBlockAlignment } from "../../lib/builder/layout-styles";
|
|
13
13
|
import type { BlockLayout } from "../../lib/sanity/types";
|
|
@@ -65,14 +65,29 @@ export default function SortableBlock({
|
|
|
65
65
|
// Only force width:100% when no horizontal alignment — align-self needs width:auto to shrink
|
|
66
66
|
const hasHAlign = blockLayout?.align_h && blockLayout.align_h !== "left";
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
// Fill-mode blocks act as column backgrounds — absolute positioned behind other blocks
|
|
69
|
+
const isFillBlock =
|
|
70
|
+
(block._type === "imageBlock" && (block as ImageBlock).width === "fill") ||
|
|
71
|
+
(block._type === "videoBlock" && (block as VideoBlock).width === "fill");
|
|
72
|
+
|
|
73
|
+
const style: React.CSSProperties = isFillBlock
|
|
74
|
+
? {
|
|
75
|
+
position: "relative",
|
|
76
|
+
flex: "999 1 0%",
|
|
77
|
+
minHeight: 0,
|
|
78
|
+
zIndex: 0,
|
|
79
|
+
opacity: isDragging ? 0.3 : 1,
|
|
80
|
+
transform: CSS.Transform.toString(transform),
|
|
81
|
+
transition,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
transform: CSS.Transform.toString(transform),
|
|
85
|
+
transition,
|
|
86
|
+
opacity: isDragging ? 0.3 : 1,
|
|
87
|
+
...(!hasHAlign ? { width: "100%" } : {}),
|
|
88
|
+
minWidth: 0,
|
|
89
|
+
...alignStyles,
|
|
90
|
+
};
|
|
76
91
|
|
|
77
92
|
const showToolbar = isSelected || isHovered;
|
|
78
93
|
|
|
@@ -106,8 +121,8 @@ export default function SortableBlock({
|
|
|
106
121
|
return (
|
|
107
122
|
<div
|
|
108
123
|
ref={setNodeRef}
|
|
109
|
-
style={style}
|
|
110
|
-
className={`
|
|
124
|
+
style={{ ...style, ...(!isFillBlock ? { position: "relative" as const, zIndex: 1 } : {}) }}
|
|
125
|
+
className={`transition-[opacity,box-shadow] ${
|
|
111
126
|
isDragging
|
|
112
127
|
? "ring-2 ring-[#0d9668] ring-offset-1 ring-offset-transparent rounded"
|
|
113
128
|
: ""
|
|
@@ -113,6 +113,7 @@ export default function ImageBlockEditor({ block }: Props) {
|
|
|
113
113
|
{ value: "full", label: "100%" },
|
|
114
114
|
{ value: "contained", label: "75%" },
|
|
115
115
|
{ value: "small", label: "50%" },
|
|
116
|
+
{ value: "fill", label: "Fill" },
|
|
116
117
|
] as const
|
|
117
118
|
).map((opt) => (
|
|
118
119
|
<button
|
|
@@ -21,8 +21,10 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
|
21
21
|
const thumbSrc = adminThumbUrl(block.asset_path);
|
|
22
22
|
const fullSrc = adminAssetUrl(block.asset_path);
|
|
23
23
|
const src = useFallback ? fullSrc : thumbSrc;
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const isFill = block.width === "fill";
|
|
25
|
+
const widthStyle = isFill
|
|
26
|
+
? "100%"
|
|
27
|
+
: block.width === "contained"
|
|
26
28
|
? "75%"
|
|
27
29
|
: block.width === "small"
|
|
28
30
|
? "50%"
|
|
@@ -35,6 +37,23 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
|
35
37
|
"21:9": "21/9",
|
|
36
38
|
};
|
|
37
39
|
|
|
40
|
+
if (isFill) {
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ width: "100%", height: "100%", overflow: "hidden", borderRadius: block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined }}>
|
|
43
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
44
|
+
<img
|
|
45
|
+
src={src}
|
|
46
|
+
alt={block.alt || ""}
|
|
47
|
+
onLoad={() => setImgLoaded(true)}
|
|
48
|
+
onError={() => {
|
|
49
|
+
if (!useFallback) { setUseFallback(true); } else { setImgError(true); }
|
|
50
|
+
}}
|
|
51
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
return (
|
|
39
58
|
<div style={{ width: widthStyle, margin: block.width !== "full" ? "0 auto" : undefined }}>
|
|
40
59
|
{imgError ? (
|
|
@@ -30,8 +30,9 @@ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
|
|
|
30
30
|
"4:3": "75%",
|
|
31
31
|
auto: "56.25%",
|
|
32
32
|
};
|
|
33
|
+
const isFill = block.width === "fill";
|
|
33
34
|
const paddingBottom = aspectMap[block.aspect_ratio || "16:9"] || "56.25%";
|
|
34
|
-
const widthStyle = block.width === "contained" ? "75%" : "100%";
|
|
35
|
+
const widthStyle = isFill ? "100%" : (block.width === "contained" ? "75%" : "100%");
|
|
35
36
|
|
|
36
37
|
// Resolve thumbnail URL based on video type (no iframes, no streaming)
|
|
37
38
|
let thumbnailUrl: string | null = null;
|
|
@@ -59,9 +60,13 @@ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
|
|
|
59
60
|
|
|
60
61
|
const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
|
|
61
62
|
|
|
63
|
+
const outerStyle: React.CSSProperties = isFill
|
|
64
|
+
? { width: "100%", height: "100%", minWidth: 0, borderRadius, overflow: "hidden" }
|
|
65
|
+
: { width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined };
|
|
66
|
+
|
|
62
67
|
return (
|
|
63
|
-
<div style={
|
|
64
|
-
<div style={{ position: "relative", paddingBottom, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
|
|
68
|
+
<div style={outerStyle}>
|
|
69
|
+
<div style={{ position: "relative", paddingBottom: isFill ? undefined : paddingBottom, height: isFill ? "100%" : undefined, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
|
|
65
70
|
{thumbnailUrl ? (
|
|
66
71
|
<>
|
|
67
72
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
|
+
import type { CoverSection } from "../../../lib/sanity/types";
|
|
5
|
+
import {
|
|
6
|
+
SpacingIcon,
|
|
7
|
+
OffsetIcon,
|
|
8
|
+
BorderIcon,
|
|
9
|
+
} from "../editors/section-icons";
|
|
10
|
+
import { SettingsField, SettingsSection } from "../editors/shared";
|
|
11
|
+
import { TRBLInputs } from "./TRBLInputs";
|
|
12
|
+
|
|
13
|
+
interface CoverSectionLayoutTabProps {
|
|
14
|
+
section: CoverSection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function CoverSectionLayoutTab({ section }: CoverSectionLayoutTabProps) {
|
|
18
|
+
const store = useBuilderStore();
|
|
19
|
+
const s = section.settings;
|
|
20
|
+
|
|
21
|
+
const update = (fields: Record<string, string | undefined>) => {
|
|
22
|
+
store.updateCoverSettings(section._key, fields as Partial<typeof s>);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{/* Spacing (padding) */}
|
|
28
|
+
<SettingsSection title="Spacing" defaultOpen icon={<SpacingIcon />}>
|
|
29
|
+
<TRBLInputs
|
|
30
|
+
top={s.spacing_top || ""}
|
|
31
|
+
right={s.spacing_right || ""}
|
|
32
|
+
bottom={s.spacing_bottom || ""}
|
|
33
|
+
left={s.spacing_left || ""}
|
|
34
|
+
onChange={(field, value) => update({ [`spacing_${field}`]: value || undefined })}
|
|
35
|
+
/>
|
|
36
|
+
</SettingsSection>
|
|
37
|
+
|
|
38
|
+
{/* Offset (margin) */}
|
|
39
|
+
<SettingsSection title="Offset" defaultOpen={false} icon={<OffsetIcon />}>
|
|
40
|
+
<TRBLInputs
|
|
41
|
+
top={s.offset_top || ""}
|
|
42
|
+
right={s.offset_right || ""}
|
|
43
|
+
bottom={s.offset_bottom || ""}
|
|
44
|
+
left={s.offset_left || ""}
|
|
45
|
+
onChange={(field, value) => update({ [`offset_${field}`]: value || undefined })}
|
|
46
|
+
/>
|
|
47
|
+
</SettingsSection>
|
|
48
|
+
|
|
49
|
+
{/* Border Radius */}
|
|
50
|
+
<SettingsSection title="Border" defaultOpen={false} icon={<BorderIcon />}>
|
|
51
|
+
<SettingsField label="Radius">
|
|
52
|
+
<div className="flex items-center gap-2">
|
|
53
|
+
<input
|
|
54
|
+
type="range"
|
|
55
|
+
min={0}
|
|
56
|
+
max={50}
|
|
57
|
+
step={1}
|
|
58
|
+
value={parseInt(s.border_radius || "0", 10) || 0}
|
|
59
|
+
onMouseDown={() => store._pushSnapshot()}
|
|
60
|
+
onChange={(e) => update({ border_radius: e.target.value === "0" ? undefined : e.target.value })}
|
|
61
|
+
className="flex-1 accent-[#076bff]"
|
|
62
|
+
/>
|
|
63
|
+
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
64
|
+
{parseInt(s.border_radius || "0", 10) || 0}px
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
</SettingsField>
|
|
68
|
+
</SettingsSection>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
package/lib/sanity/queries.ts
CHANGED
|
@@ -5,14 +5,14 @@ import { groq } from "next-sanity";
|
|
|
5
5
|
// ============================================
|
|
6
6
|
|
|
7
7
|
// Deep expansion of content_rows.
|
|
8
|
-
// Handles PageSectionV2, CustomSectionInstance, and
|
|
8
|
+
// Handles PageSectionV2, CustomSectionInstance, ParallaxGroup, and CoverSection.
|
|
9
9
|
// GROQ projections are additive — fields that don't exist on an object are omitted.
|
|
10
10
|
const blockExpansion = `
|
|
11
11
|
content_rows[] {
|
|
12
12
|
_key,
|
|
13
13
|
_type,
|
|
14
14
|
section_type,
|
|
15
|
-
// ── V2 section columns ──
|
|
15
|
+
// ── V2 section columns (shared by PageSectionV2 and CoverSection) ──
|
|
16
16
|
columns[] {
|
|
17
17
|
_key,
|
|
18
18
|
_type,
|
|
@@ -73,9 +73,23 @@ const blockExpansion = `
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
-
// ──
|
|
76
|
+
// ── CoverSection fields (only present when _type == "coverSection") ──
|
|
77
|
+
background_type,
|
|
78
|
+
background_image,
|
|
79
|
+
background_video,
|
|
80
|
+
background_position,
|
|
81
|
+
background_size,
|
|
82
|
+
background_overlay_color,
|
|
83
|
+
background_overlay_opacity,
|
|
84
|
+
height,
|
|
85
|
+
cover_rows[] {
|
|
86
|
+
_key,
|
|
87
|
+
height_percent,
|
|
88
|
+
vertical_align
|
|
89
|
+
},
|
|
90
|
+
// ── Settings (V2 section settings + CoverSection settings) ──
|
|
77
91
|
settings,
|
|
78
|
-
// ── Responsive overrides (V2
|
|
92
|
+
// ── Responsive overrides (V2 + CoverSection store responsive at section level) ──
|
|
79
93
|
responsive
|
|
80
94
|
}
|
|
81
95
|
`;
|
package/lib/sanity/types.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface ImageBlock {
|
|
|
135
135
|
asset_path: string;
|
|
136
136
|
alt?: string;
|
|
137
137
|
caption?: string;
|
|
138
|
-
width?: "full" | "contained" | "small";
|
|
138
|
+
width?: "full" | "contained" | "small" | "fill";
|
|
139
139
|
aspect_ratio?: "auto" | "16:9" | "4:3" | "1:1" | "21:9";
|
|
140
140
|
lazy?: boolean;
|
|
141
141
|
border_radius?: string;
|
|
@@ -174,7 +174,7 @@ export interface VideoBlock {
|
|
|
174
174
|
loop?: boolean;
|
|
175
175
|
muted?: boolean;
|
|
176
176
|
controls?: boolean;
|
|
177
|
-
width?: "full" | "contained";
|
|
177
|
+
width?: "full" | "contained" | "fill";
|
|
178
178
|
aspect_ratio?: "16:9" | "21:9" | "4:3" | "auto";
|
|
179
179
|
border_radius?: string;
|
|
180
180
|
enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
|
|
@@ -452,15 +452,24 @@ export interface CoverRow {
|
|
|
452
452
|
vertical_align: "start" | "center" | "end";
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
/** Cover Section settings (subset of SectionV2Settings
|
|
455
|
+
/** Cover Section settings (subset of SectionV2Settings) */
|
|
456
456
|
export interface CoverSectionSettings {
|
|
457
457
|
grid_columns: number; // default 12
|
|
458
458
|
col_gap: number;
|
|
459
459
|
row_gap: number;
|
|
460
|
+
// Spacing (padding TRBL, px)
|
|
460
461
|
spacing_top?: string;
|
|
461
462
|
spacing_right?: string;
|
|
462
463
|
spacing_bottom?: string;
|
|
463
464
|
spacing_left?: string;
|
|
465
|
+
// Offset (margin TRBL, px)
|
|
466
|
+
offset_top?: string;
|
|
467
|
+
offset_right?: string;
|
|
468
|
+
offset_bottom?: string;
|
|
469
|
+
offset_left?: string;
|
|
470
|
+
// Border
|
|
471
|
+
border_radius?: string;
|
|
472
|
+
// Animation
|
|
464
473
|
enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
|
|
465
474
|
stagger?: {
|
|
466
475
|
enabled?: boolean;
|
package/lib/version.ts
CHANGED
package/package.json
CHANGED
|
@@ -95,6 +95,10 @@ const responsiveSettingsFields = [
|
|
|
95
95
|
defineField({ name: "spacing_right", type: "string", title: "Spacing Right" }),
|
|
96
96
|
defineField({ name: "spacing_bottom", type: "string", title: "Spacing Bottom" }),
|
|
97
97
|
defineField({ name: "spacing_left", type: "string", title: "Spacing Left" }),
|
|
98
|
+
defineField({ name: "offset_top", type: "string", title: "Offset Top" }),
|
|
99
|
+
defineField({ name: "offset_right", type: "string", title: "Offset Right" }),
|
|
100
|
+
defineField({ name: "offset_bottom", type: "string", title: "Offset Bottom" }),
|
|
101
|
+
defineField({ name: "offset_left", type: "string", title: "Offset Left" }),
|
|
98
102
|
];
|
|
99
103
|
|
|
100
104
|
export default defineType({
|
|
@@ -250,6 +254,13 @@ export default defineType({
|
|
|
250
254
|
defineField({ name: "spacing_right", title: "Spacing Right", type: "string" }),
|
|
251
255
|
defineField({ name: "spacing_bottom", title: "Spacing Bottom", type: "string" }),
|
|
252
256
|
defineField({ name: "spacing_left", title: "Spacing Left", type: "string" }),
|
|
257
|
+
// Offset (margin TRBL)
|
|
258
|
+
defineField({ name: "offset_top", title: "Offset Top", type: "string" }),
|
|
259
|
+
defineField({ name: "offset_right", title: "Offset Right", type: "string" }),
|
|
260
|
+
defineField({ name: "offset_bottom", title: "Offset Bottom", type: "string" }),
|
|
261
|
+
defineField({ name: "offset_left", title: "Offset Left", type: "string" }),
|
|
262
|
+
// Border
|
|
263
|
+
defineField({ name: "border_radius", title: "Border Radius", type: "string" }),
|
|
253
264
|
// Animation
|
|
254
265
|
defineField({
|
|
255
266
|
name: "enter_animation",
|