@morphika/andami 0.2.14 → 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.
@@ -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
- alignSelf,
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={alignStyles}>
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
  );
@@ -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={{ ...(!hasHAlign ? { width: "100%" } : { width: "auto", maxWidth: "100%" }), minWidth: 0, ...alignStyles }}>
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}
@@ -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
- const style: React.CSSProperties = {
69
- transform: CSS.Transform.toString(transform),
70
- transition,
71
- opacity: isDragging ? 0.3 : 1,
72
- ...(!hasHAlign ? { width: "100%" } : {}),
73
- minWidth: 0,
74
- ...alignStyles,
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={`relative transition-[opacity,box-shadow] ${
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
  : ""
@@ -39,7 +39,7 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
39
39
 
40
40
  if (isFill) {
41
41
  return (
42
- <div style={{ position: "absolute", inset: 0, overflow: "hidden", borderRadius: block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined }}>
42
+ <div style={{ width: "100%", height: "100%", overflow: "hidden", borderRadius: block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined }}>
43
43
  {/* eslint-disable-next-line @next/next/no-img-element */}
44
44
  <img
45
45
  src={src}
@@ -61,7 +61,7 @@ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
61
61
  const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
62
62
 
63
63
  const outerStyle: React.CSSProperties = isFill
64
- ? { position: "absolute", inset: 0, minWidth: 0, borderRadius, overflow: "hidden" }
64
+ ? { width: "100%", height: "100%", minWidth: 0, borderRadius, overflow: "hidden" }
65
65
  : { width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined };
66
66
 
67
67
  return (
@@ -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
+ }
@@ -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 — no background/border) */
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
@@ -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.14";
9
+ export const ANDAMI_VERSION = "0.2.15";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
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",
@@ -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",