@morphika/andami 0.2.13 → 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.
@@ -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 widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
29
- const aspect = aspectMap[block.aspect_ratio ?? "auto"];
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={{ ...widthStyle, borderRadius, overflow: "hidden" }}>
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>
@@ -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 widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
290
- const paddingBottom = aspectMap[block.aspect_ratio ?? "16:9"] || "56.25%";
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={{ ...widthStyle, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
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
  );
@@ -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
@@ -192,6 +192,7 @@ export default function VideoBlockEditor({ block }: Props) {
192
192
  [
193
193
  { value: "full", label: "Full" },
194
194
  { value: "contained", label: "Contained" },
195
+ { value: "fill", label: "Fill" },
195
196
  ] as const
196
197
  ).map((opt) => (
197
198
  <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 widthStyle =
25
- block.width === "contained"
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={{ position: "absolute", inset: 0, 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
+ ? { position: "absolute", inset: 0, 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={{ width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
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 */}
@@ -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 ParallaxGroup.
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
- // ── Settings (V2 section settings) ──
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 sections store responsive at section level) ──
92
+ // ── Responsive overrides (V2 + CoverSection store responsive at section level) ──
79
93
  responsive
80
94
  }
81
95
  `;
@@ -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;
package/lib/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * Exposed as a plain constant so it can be imported without reading
7
7
  * package.json at runtime.
8
8
  */
9
- export const ANDAMI_VERSION = "0.2.13";
9
+ export const ANDAMI_VERSION = "0.2.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@ export const imageBlock = defineType({
24
24
  { title: "Full", value: "full" },
25
25
  { title: "Contained", value: "contained" },
26
26
  { title: "Small", value: "small" },
27
+ { title: "Fill", value: "fill" },
27
28
  ],
28
29
  },
29
30
  initialValue: "full",
@@ -45,6 +45,7 @@ export const videoBlock = defineType({
45
45
  list: [
46
46
  { title: "Full", value: "full" },
47
47
  { title: "Contained", value: "contained" },
48
+ { title: "Fill", value: "fill" },
48
49
  ],
49
50
  },
50
51
  initialValue: "full",