@morphika/andami 0.5.8 → 0.5.10
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 +2 -2
- package/components/blocks/SectionV2Renderer.tsx +50 -17
- package/components/builder/ReadOnlyFrame.tsx +4 -4
- package/components/builder/SectionV2Column.tsx +18 -5
- package/lib/builder/layout-styles.ts +55 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
274
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
@@ -499,11 +502,21 @@ export default function SectionV2Column({
|
|
|
499
502
|
)}
|
|
500
503
|
</SortableContext>
|
|
501
504
|
|
|
502
|
-
{/* "+" add block button below blocks
|
|
505
|
+
{/* "+" add block button below blocks.
|
|
506
|
+
When the column has a vertical alignment (center / flex-end via
|
|
507
|
+
colJustify), this drop zone is anchored absolutely to the bottom of
|
|
508
|
+
the column so its `flex: 1` doesn't eat the spare space and break
|
|
509
|
+
the column's `justify-content`. Without alignment (top default), it
|
|
510
|
+
keeps the legacy `flex: 1` so the entire empty area below the last
|
|
511
|
+
block remains a generous drop target. */}
|
|
503
512
|
{/* Hidden for section-level blocks (e.g. projectGridBlock) that own the full column */}
|
|
504
513
|
{hasBlocks && !singleSectionBlock && (
|
|
505
514
|
<div
|
|
506
|
-
className={`flex
|
|
515
|
+
className={`flex items-center justify-center z-[3] transition-all ${
|
|
516
|
+
colJustify
|
|
517
|
+
? "absolute left-0 right-0 bottom-0"
|
|
518
|
+
: "flex-1 min-h-0"
|
|
519
|
+
} ${
|
|
507
520
|
showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
|
|
508
521
|
}`}
|
|
509
522
|
style={{ minHeight: 24 }}
|
|
@@ -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
package/package.json
CHANGED