@morphika/andami 0.5.8 → 0.5.9

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.
@@ -21,7 +21,7 @@ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
21
21
  import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
22
22
  import BlockRenderer from "./BlockRenderer";
23
23
  import EnterAnimationWrapper from "./EnterAnimationWrapper";
24
- import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, getBackgroundStyles, getBorderStyles } from "../../lib/builder/layout-styles";
24
+ import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlignForViewport, getBackgroundStyles, getBorderStyles } from "../../lib/builder/layout-styles";
25
25
  import { assetUrl } from "../../lib/assets";
26
26
  import { BREAKPOINTS } from "../../lib/builder/constants";
27
27
  import { normalizeRowHeights } from "../../lib/builder/store-cover";
@@ -303,7 +303,7 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
303
303
  const staggerIdx = staggerEnabled ? getStaggerIndex(colIndex) : undefined;
304
304
  const rowAlign = rowAlignMap[String(col.grid_row)] || "start";
305
305
  const alignSelf = rowAlign === "center" ? "center" : rowAlign === "end" ? "end" : "start";
306
- const colJustify = getColumnVerticalAlign(col.blocks || []);
306
+ const colJustify = getColumnVerticalAlignForViewport(col.blocks || [], "desktop");
307
307
 
308
308
  const colLayoutStyles = {
309
309
  ...getBackgroundStyles(col, process.env.NEXT_PUBLIC_ASSET_BASE_URL),
@@ -24,7 +24,7 @@ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
24
24
  import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
25
25
  import BlockRenderer from "./BlockRenderer";
26
26
  import EnterAnimationWrapper from "./EnterAnimationWrapper";
27
- import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, getBackgroundStyles, getBorderStyles, hexToRgba } from "../../lib/builder/layout-styles";
27
+ import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlignForViewport, getBackgroundStyles, getBorderStyles, hexToRgba } from "../../lib/builder/layout-styles";
28
28
  import { parseColorField, colorToOverrideRule, borderColorToOverrideRule } from "../../lib/color-utils";
29
29
  import { BREAKPOINTS } from "../../lib/builder/constants";
30
30
 
@@ -159,27 +159,57 @@ function buildColumnOverrideCss(
159
159
 
160
160
  /**
161
161
  * Generate all responsive CSS for a V2 section (section settings + column overrides).
162
+ *
163
+ * Includes column-level `justify-content` (vertical alignment) overrides when a
164
+ * block's responsive `align_v` differs from the desktop default. The desktop
165
+ * value is applied inline; this function emits the tablet/phone deltas.
162
166
  */
163
167
  function buildSectionV2ResponsiveCss(section: PageSectionV2): string | null {
164
- const responsive = section.responsive;
165
- if (!responsive) return null;
166
-
167
168
  const key = section._key;
168
169
  const cssParts: string[] = [];
169
170
 
170
- for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
171
- const override = responsive[vp];
172
- if (!override) continue;
173
-
174
- // Section-level settings overrides
175
- const settingsRules = buildSettingsOverrideRules(override.settings as Partial<SectionV2Settings> | undefined);
176
- if (settingsRules.length > 0) {
177
- cssParts.push(`@media(max-width:${breakpoint}px){.sv2-${key}{${settingsRules.join(";")}}}`);
171
+ // Section-level settings overrides + column position/span overrides
172
+ const responsive = section.responsive;
173
+ if (responsive) {
174
+ for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
175
+ const override = responsive[vp];
176
+ if (!override) continue;
177
+
178
+ const settingsRules = buildSettingsOverrideRules(override.settings as Partial<SectionV2Settings> | undefined);
179
+ if (settingsRules.length > 0) {
180
+ cssParts.push(`@media(max-width:${breakpoint}px){.sv2-${key}{${settingsRules.join(";")}}}`);
181
+ }
182
+
183
+ const colCss = buildColumnOverrideCss(key, override.columns, breakpoint);
184
+ if (colCss) cssParts.push(colCss);
178
185
  }
186
+ }
179
187
 
180
- // Column position/span overrides
181
- const colCss = buildColumnOverrideCss(key, override.columns, breakpoint);
182
- if (colCss) cssParts.push(colCss);
188
+ // Column-level vertical alignment overrides (always evaluated — these live on
189
+ // the blocks themselves at `block.responsive[vp].layout.align_v`, not on
190
+ // section.responsive). For each column, if the resolved colJustify for a
191
+ // given viewport differs from the previous viewport (desktop → tablet,
192
+ // tablet → phone), emit a media query rule with `!important`.
193
+ const tabletRules: string[] = [];
194
+ const phoneRules: string[] = [];
195
+ for (const col of section.columns) {
196
+ const blocks = col.blocks || [];
197
+ const desktop = getColumnVerticalAlignForViewport(blocks, "desktop");
198
+ const tablet = getColumnVerticalAlignForViewport(blocks, "tablet");
199
+ const phone = getColumnVerticalAlignForViewport(blocks, "phone");
200
+
201
+ if (tablet !== desktop) {
202
+ tabletRules.push(`.sv2-col-${key}-${col._key}{justify-content:${tablet || "flex-start"}!important}`);
203
+ }
204
+ if (phone !== tablet) {
205
+ phoneRules.push(`.sv2-col-${key}-${col._key}{justify-content:${phone || "flex-start"}!important}`);
206
+ }
207
+ }
208
+ if (tabletRules.length > 0) {
209
+ cssParts.push(`@media(max-width:${BREAKPOINTS.tablet}px){${tabletRules.join("")}}`);
210
+ }
211
+ if (phoneRules.length > 0) {
212
+ cssParts.push(`@media(max-width:${BREAKPOINTS.phone}px){${phoneRules.join("")}}`);
183
213
  }
184
214
 
185
215
  return cssParts.length > 0 ? cssParts.join("") : null;
@@ -270,8 +300,11 @@ export default function SectionV2Renderer({ section, pageEnterAnimation, fillHei
270
300
  {sortedColumns.map((col, colIndex) => {
271
301
  const staggerIdx = staggerEnabled ? getStaggerIndex(colIndex) : undefined;
272
302
 
273
- // Column-level vertical alignment from blocks' align_v settings
274
- const colJustify = getColumnVerticalAlign(col.blocks || []);
303
+ // Column-level vertical alignment, viewport-aware. Desktop value is
304
+ // applied inline; tablet/phone overrides (when they differ) are
305
+ // emitted as media-query CSS below so the live site honours
306
+ // responsive `align_v` overrides authored in the builder.
307
+ const colJustify = getColumnVerticalAlignForViewport(col.blocks || [], "desktop");
275
308
 
276
309
  // Column-level background + border (desktop-only).
277
310
  const colLayoutStyles = {
@@ -24,7 +24,7 @@ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSecti
24
24
  import { DEVICE_HEIGHTS } from "../../lib/builder/types";
25
25
  import { getEffectiveColumnsV2, getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
26
26
  import BlockLivePreview from "./BlockLivePreview";
27
- import { getColumnVerticalAlign, getRowLayoutStyles } from "../../lib/builder/layout-styles";
27
+ import { getColumnVerticalAlignForViewport, getRowLayoutStyles } from "../../lib/builder/layout-styles";
28
28
  import type { ContentBlock } from "../../lib/sanity/types";
29
29
 
30
30
  function isFillBlock(block: ContentBlock): boolean {
@@ -82,7 +82,7 @@ const ReadOnlySectionV2 = memo(function ReadOnlySectionV2({ section, viewport }:
82
82
  const gridColumn = eff?.grid_column ?? col.grid_column;
83
83
  const gridRow = eff?.grid_row ?? col.grid_row;
84
84
  const span = eff?.span ?? col.span;
85
- const colJustify = getColumnVerticalAlign(col.blocks || []);
85
+ const colJustify = getColumnVerticalAlignForViewport(col.blocks || [], viewport);
86
86
 
87
87
  return (
88
88
  <div
@@ -198,7 +198,7 @@ const ReadOnlyParallaxSlide = memo(function ReadOnlyParallaxSlide({
198
198
  }}
199
199
  >
200
200
  {slide.columns.map((col) => {
201
- const colJustify = getColumnVerticalAlign(col.blocks || []);
201
+ const colJustify = getColumnVerticalAlignForViewport(col.blocks || [], viewport);
202
202
  return (
203
203
  <div
204
204
  key={col._key}
@@ -439,7 +439,7 @@ const ReadOnlyCoverSection = memo(function ReadOnlyCoverSection({
439
439
  >
440
440
  {section.columns.map((col) => {
441
441
  const rowAlign = effectiveRows[col.grid_row - 1]?.vertical_align || "start";
442
- const colJustify = getColumnVerticalAlign(col.blocks || []);
442
+ const colJustify = getColumnVerticalAlignForViewport(col.blocks || [], viewport);
443
443
  const justify = rowAlign === "center" ? "center" : rowAlign === "end" ? "flex-end" : colJustify || "flex-start";
444
444
  return (
445
445
  <div
@@ -10,7 +10,7 @@ import { useBuilderStore } from "../../lib/builder/store";
10
10
  import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
11
11
  import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
12
12
  import {
13
- getColumnVerticalAlign,
13
+ getColumnVerticalAlignForViewport,
14
14
  getBackgroundStyles,
15
15
  getBorderStyles,
16
16
  } from "../../lib/builder/layout-styles";
@@ -165,6 +165,7 @@ export default function SectionV2Column({
165
165
  }: SectionV2ColumnProps) {
166
166
  const previewMode = useBuilderStore((s) => s.previewMode);
167
167
  const canvasZoom = useBuilderStore((s) => s.canvasZoom);
168
+ const activeViewport = useBuilderStore((s) => s.activeViewport);
168
169
  const [isHovered, setIsHovered] = useState(false);
169
170
  const [resizingEdge, setResizingEdge] = useState<"left" | "right" | null>(null);
170
171
  const [hoveredEdge, setHoveredEdge] = useState<"left" | "right" | null>(null);
@@ -231,8 +232,10 @@ export default function SectionV2Column({
231
232
  // Show faint outlines when section is hovered but not this specific column
232
233
  const showFaintOutline = isSectionHovered && !isHovered && !isSelected && !isDraggedColumn && !isLockedColumn;
233
234
 
234
- // Column-level vertical alignment from blocks' align_v settings
235
- const colJustify = getColumnVerticalAlign(column.blocks || []);
235
+ // Column-level vertical alignment from blocks' align_v settings.
236
+ // Viewport-aware: respects responsive overrides for the active viewport
237
+ // (active canvas frame edits the viewport selected in the toolbar).
238
+ const colJustify = getColumnVerticalAlignForViewport(column.blocks || [], activeViewport);
236
239
 
237
240
  // Column-level background + border (desktop-only — no responsive overrides).
238
241
  const columnLayoutStyles: React.CSSProperties = {
@@ -303,6 +303,61 @@ export function getColumnVerticalAlign(blocks: Array<{ layout?: BlockLayout }>):
303
303
  return undefined; // default (flex-start)
304
304
  }
305
305
 
306
+ /**
307
+ * Resolve a single block's effective `align_v` for a given viewport, honouring
308
+ * responsive overrides stored at `block.responsive[viewport].layout.align_v`
309
+ * and falling back to the desktop default at `block.layout.align_v`.
310
+ *
311
+ * Accepts a loosely-typed block — callers pass `ContentBlock` shapes whose
312
+ * concrete `responsive` generic does not satisfy a structural index signature,
313
+ * so we read via `unknown` cast (same pattern as `getColumnVerticalAlign`).
314
+ */
315
+ function resolveBlockAlignV(
316
+ block: { layout?: BlockLayout },
317
+ viewport: "desktop" | "tablet" | "phone"
318
+ ): "top" | "center" | "bottom" | undefined {
319
+ if (viewport !== "desktop") {
320
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
321
+ | Record<string, { layout?: Partial<BlockLayout> }>
322
+ | undefined;
323
+ const vpLayout = responsive?.[viewport]?.layout;
324
+ if (vpLayout && "align_v" in vpLayout) {
325
+ return vpLayout.align_v as "top" | "center" | "bottom" | undefined;
326
+ }
327
+ }
328
+ const layout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
329
+ return layout?.align_v as "top" | "center" | "bottom" | undefined;
330
+ }
331
+
332
+ /**
333
+ * Viewport-aware variant of {@link getColumnVerticalAlign}. Resolves each
334
+ * block's effective `align_v` for the given viewport (responsive override
335
+ * cascading down to desktop default) and applies the same column-level
336
+ * priority (bottom > center > top).
337
+ *
338
+ * Used by:
339
+ * - Builder canvas (active viewport from store)
340
+ * - ReadOnlyFrame (per-frame viewport prop)
341
+ * - Public SectionV2Renderer (desktop default + media queries for overrides)
342
+ */
343
+ export function getColumnVerticalAlignForViewport(
344
+ blocks: Array<{ layout?: BlockLayout }>,
345
+ viewport: "desktop" | "tablet" | "phone"
346
+ ): string | undefined {
347
+ let hasBottom = false;
348
+ let hasCenter = false;
349
+
350
+ for (const block of blocks) {
351
+ const v = resolveBlockAlignV(block, viewport);
352
+ if (v === "bottom") hasBottom = true;
353
+ else if (v === "center") hasCenter = true;
354
+ }
355
+
356
+ if (hasBottom) return "flex-end";
357
+ if (hasCenter) return "center";
358
+ return undefined; // default (flex-start)
359
+ }
360
+
306
361
  /**
307
362
  * Check if a block has any layout styles set.
308
363
  */
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.5.8";
9
+ export const ANDAMI_VERSION = "0.5.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
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",