@morphika/andami 0.1.9 → 0.2.0
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/app/admin/pages/[slug]/page.tsx +3 -7
- package/app/api/admin/pages/[slug]/route.ts +2 -28
- package/app/api/admin/settings/route.ts +30 -0
- package/components/admin/nav-builder/NavBuilder.tsx +90 -14
- package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
- package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
- package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
- package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
- package/components/blocks/EnterAnimationWrapper.tsx +19 -4
- package/components/blocks/PageRenderer.tsx +2 -15
- package/components/blocks/ProjectGridBlockRenderer.tsx +34 -36
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/DndWrapper.tsx +2 -24
- package/components/builder/InsertionLines.tsx +5 -5
- package/components/builder/ReadOnlyFrame.tsx +5 -49
- package/components/builder/SectionV2Canvas.tsx +2 -2
- package/components/builder/SectionV2Column.tsx +5 -5
- package/components/builder/SettingsPanel.tsx +0 -12
- package/components/builder/SortableBlock.tsx +3 -3
- package/components/builder/SortableRow.tsx +6 -27
- package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
- package/components/builder/editors/CoverBlockEditor.tsx +14 -6
- package/components/builder/editors/ImageBlockEditor.tsx +8 -3
- package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
- package/components/builder/editors/ProjectGridEditor.tsx +7 -46
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
- package/components/builder/editors/StaggerSettings.tsx +2 -1
- package/components/builder/editors/TextBlockEditor.tsx +8 -3
- package/components/builder/editors/VideoBlockEditor.tsx +10 -4
- package/components/builder/editors/section-icons.tsx +492 -0
- package/components/builder/editors/shared.tsx +23 -4
- package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -3
- package/components/builder/live-preview/drag-utils.tsx +2 -2
- package/components/builder/settings-panel/AnimationTab.tsx +2 -16
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- package/components/builder/settings-panel/PageSettings.tsx +10 -4
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
- package/components/builder/settings-panel/index.ts +0 -1
- package/components/builder/settings-panel/responsive-helpers.ts +2 -50
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/constants.ts +5 -4
- package/lib/builder/serializer/normalizers.ts +2 -40
- package/lib/builder/serializer/serializers.ts +3 -74
- package/lib/builder/store-blocks.ts +3 -19
- package/lib/builder/store-helpers.ts +2 -2
- package/lib/builder/store-sections.ts +26 -64
- package/lib/builder/store.ts +3 -6
- package/lib/builder/templates.ts +9 -45
- package/lib/builder/types.ts +4 -11
- package/lib/sanity/queries.ts +6 -29
- package/lib/sanity/types.ts +24 -70
- package/package.json +4 -1
- package/sanity/schemas/index.ts +0 -5
- package/sanity/schemas/objects/parallaxGroup.ts +2 -2
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +1 -0
- package/sanity/schemas/siteSettings.ts +42 -0
- package/styles/base.css +8 -2
- package/components/blocks/SectionRenderer.tsx +0 -171
- package/components/builder/settings-panel/LayoutTab.tsx +0 -382
- package/sanity/schemas/pageSection.ts +0 -157
package/components/ui/Navbar.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import type { NavItem, NavDesign, NavEntrancePreset, MobileNavDesign } from "../../lib/sanity/types";
|
|
6
|
+
import type { NavItem, NavDesign, NavEntrancePreset, NavDesignResponsiveOverride, MobileNavDesign } from "../../lib/sanity/types";
|
|
7
7
|
import { useNavColor } from "../../lib/contexts/NavColorContext";
|
|
8
8
|
import { useNavAnimation } from "../../lib/contexts/NavAnimationContext";
|
|
9
9
|
import { usePageExit } from "../../lib/contexts/PageExitContext";
|
|
@@ -23,6 +23,79 @@ const colorMap: Record<string, string> = {
|
|
|
23
23
|
white: "text-brand-text",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// ============================================
|
|
27
|
+
// Responsive CSS custom properties generator
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
/** Breakpoints matching the builder's responsive system */
|
|
31
|
+
const NAV_BREAKPOINTS = { tablet: 1024, phone: 640 } as const;
|
|
32
|
+
|
|
33
|
+
/** Generates a <style> block with CSS custom properties for responsive nav overrides.
|
|
34
|
+
* Desktop values are set on the scoping selector; tablet/phone overrides via media queries.
|
|
35
|
+
* Returns empty string if no responsive overrides exist. */
|
|
36
|
+
function buildResponsiveNavCSS(design: NavDesign | undefined, scopeSelector: string): string {
|
|
37
|
+
if (!design) return "";
|
|
38
|
+
|
|
39
|
+
// Desktop (base) custom properties
|
|
40
|
+
const vars: Record<string, string> = {
|
|
41
|
+
"--nav-font-size": `${design.font_size ?? 14}px`,
|
|
42
|
+
"--nav-font-weight": design.font_weight || "400",
|
|
43
|
+
"--nav-text-transform": design.text_transform || "uppercase",
|
|
44
|
+
"--nav-text-align": design.text_align || "left",
|
|
45
|
+
"--nav-vertical-align": (() => {
|
|
46
|
+
const v = design.vertical_align || "top";
|
|
47
|
+
return v === "bottom" ? "end" : v === "middle" ? "center" : "start";
|
|
48
|
+
})(),
|
|
49
|
+
"--nav-items-justify": (() => {
|
|
50
|
+
const a = design.text_align || "left";
|
|
51
|
+
return a === "center" ? "center" : a === "right" ? "flex-end" : "flex-start";
|
|
52
|
+
})(),
|
|
53
|
+
"--nav-padding-h": `${design.padding_h ?? 24}px`,
|
|
54
|
+
"--nav-padding-v": `${design.padding_v ?? 27}px`,
|
|
55
|
+
"--nav-margin-h": `${design.margin_h ?? 0}px`,
|
|
56
|
+
"--nav-margin-v": `${design.margin_v ?? 0}px`,
|
|
57
|
+
"--nav-items-gap": `${design.items_gap ?? 32}px`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const responsive = design.responsive;
|
|
61
|
+
if (!responsive) {
|
|
62
|
+
// No responsive overrides — still emit base vars for consistency
|
|
63
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
64
|
+
return `${scopeSelector}{${baseLines}}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build media query blocks for tablet and phone
|
|
68
|
+
const mqBlocks: string[] = [];
|
|
69
|
+
for (const [viewport, bp] of [["tablet", NAV_BREAKPOINTS.tablet], ["phone", NAV_BREAKPOINTS.phone]] as const) {
|
|
70
|
+
const overrides = responsive[viewport] as NavDesignResponsiveOverride | undefined;
|
|
71
|
+
if (!overrides) continue;
|
|
72
|
+
const ovLines: string[] = [];
|
|
73
|
+
if (overrides.font_size != null) ovLines.push(`--nav-font-size:${overrides.font_size}px`);
|
|
74
|
+
if (overrides.font_weight != null) ovLines.push(`--nav-font-weight:${overrides.font_weight}`);
|
|
75
|
+
if (overrides.text_transform != null) ovLines.push(`--nav-text-transform:${overrides.text_transform}`);
|
|
76
|
+
if (overrides.text_align != null) {
|
|
77
|
+
ovLines.push(`--nav-text-align:${overrides.text_align}`);
|
|
78
|
+
const j = overrides.text_align === "center" ? "center" : overrides.text_align === "right" ? "flex-end" : "flex-start";
|
|
79
|
+
ovLines.push(`--nav-items-justify:${j}`);
|
|
80
|
+
}
|
|
81
|
+
if (overrides.vertical_align != null) {
|
|
82
|
+
const v = overrides.vertical_align === "bottom" ? "end" : overrides.vertical_align === "middle" ? "center" : "start";
|
|
83
|
+
ovLines.push(`--nav-vertical-align:${v}`);
|
|
84
|
+
}
|
|
85
|
+
if (overrides.padding_h != null) ovLines.push(`--nav-padding-h:${overrides.padding_h}px`);
|
|
86
|
+
if (overrides.padding_v != null) ovLines.push(`--nav-padding-v:${overrides.padding_v}px`);
|
|
87
|
+
if (overrides.margin_h != null) ovLines.push(`--nav-margin-h:${overrides.margin_h}px`);
|
|
88
|
+
if (overrides.margin_v != null) ovLines.push(`--nav-margin-v:${overrides.margin_v}px`);
|
|
89
|
+
if (overrides.items_gap != null) ovLines.push(`--nav-items-gap:${overrides.items_gap}px`);
|
|
90
|
+
if (ovLines.length > 0) {
|
|
91
|
+
mqBlocks.push(`@media(max-width:${bp}px){${scopeSelector}{${ovLines.join(";")}}}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
96
|
+
return `${scopeSelector}{${baseLines}}${mqBlocks.join("")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
// ============================================
|
|
27
100
|
// NavLink — shared link component (eliminates duplication)
|
|
28
101
|
// ============================================
|
|
@@ -181,6 +254,18 @@ export default function Navbar({
|
|
|
181
254
|
// Map vertical_align to CSS align-items value for the grid container
|
|
182
255
|
const gridAlignItems = verticalAlign === "bottom" ? "end" : verticalAlign === "middle" ? "center" : "start";
|
|
183
256
|
|
|
257
|
+
// ── Responsive CSS custom properties (Session 164) ──
|
|
258
|
+
// Uses a stable ID scoped to the nav element. CSS vars + media queries
|
|
259
|
+
// ensure zero-flash SSR: the correct values apply before hydration.
|
|
260
|
+
const hasResponsive = !!(design?.responsive?.tablet || design?.responsive?.phone);
|
|
261
|
+
const reactId = useId();
|
|
262
|
+
const navScopeId = `nav-${reactId.replace(/:/g, "")}`;
|
|
263
|
+
const responsiveCSS = useMemo(
|
|
264
|
+
() => hasResponsive ? buildResponsiveNavCSS(design, `[data-nav-scope="${navScopeId}"]`) : "",
|
|
265
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
266
|
+
[hasResponsive, design?.responsive, navScopeId, fontSize, fontWeight, textTransformVal, textAlignVal, paddingH, paddingV, marginH, marginV, itemsGap, verticalAlign],
|
|
267
|
+
);
|
|
268
|
+
|
|
184
269
|
// ── Mobile menu resolved values (Session 158) ──
|
|
185
270
|
// Mobile styles are independent from page-level NavColorContext overrides.
|
|
186
271
|
// They fall back to desktop design values when not explicitly set.
|
|
@@ -374,13 +459,23 @@ export default function Navbar({
|
|
|
374
459
|
// When using hex color, we must set `color: inherit` on links because browser
|
|
375
460
|
// user-agent stylesheet sets `a { color: ... }` which overrides CSS inheritance.
|
|
376
461
|
const linkClassName = `tracking-normal ${textColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
462
|
+
// When responsive overrides exist, link styles reference CSS custom properties
|
|
463
|
+
// so they respond to media queries without JS. Otherwise use direct values.
|
|
464
|
+
const linkStyle: React.CSSProperties = hasResponsive
|
|
465
|
+
? {
|
|
466
|
+
fontSize: "var(--nav-font-size)",
|
|
467
|
+
fontWeight: "var(--nav-font-weight)" as React.CSSProperties["fontWeight"],
|
|
468
|
+
textTransform: "var(--nav-text-transform)" as React.CSSProperties["textTransform"],
|
|
469
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
470
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
471
|
+
}
|
|
472
|
+
: {
|
|
473
|
+
fontSize: `${fontSize}px`,
|
|
474
|
+
fontWeight: fontWeight as React.CSSProperties["fontWeight"],
|
|
475
|
+
textTransform: textTransformVal as React.CSSProperties["textTransform"],
|
|
476
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
477
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
478
|
+
};
|
|
384
479
|
|
|
385
480
|
return (
|
|
386
481
|
<>
|
|
@@ -388,6 +483,7 @@ export default function Navbar({
|
|
|
388
483
|
<nav
|
|
389
484
|
role="navigation"
|
|
390
485
|
aria-label="Main navigation"
|
|
486
|
+
data-nav-scope={navScopeId}
|
|
391
487
|
className={`${positionClass} top-0 left-0 right-0 z-50 transition-transform duration-300 ease-in-out ${
|
|
392
488
|
shouldHide ? "-translate-y-full" : "translate-y-0"
|
|
393
489
|
}`}
|
|
@@ -397,14 +493,21 @@ export default function Navbar({
|
|
|
397
493
|
style={{
|
|
398
494
|
...navBgStyle,
|
|
399
495
|
...textColorStyle,
|
|
400
|
-
...(
|
|
496
|
+
...(hasResponsive
|
|
401
497
|
? {
|
|
402
|
-
left:
|
|
403
|
-
right:
|
|
404
|
-
top:
|
|
405
|
-
borderRadius: "8px",
|
|
498
|
+
left: "var(--nav-margin-h)",
|
|
499
|
+
right: "var(--nav-margin-h)",
|
|
500
|
+
top: "var(--nav-margin-v)",
|
|
501
|
+
...(marginH > 0 || marginV > 0 ? { borderRadius: "8px" } : {}),
|
|
406
502
|
}
|
|
407
|
-
:
|
|
503
|
+
: marginH > 0 || marginV > 0
|
|
504
|
+
? {
|
|
505
|
+
left: `${marginH}px`,
|
|
506
|
+
right: `${marginH}px`,
|
|
507
|
+
top: `${marginV}px`,
|
|
508
|
+
borderRadius: "8px",
|
|
509
|
+
}
|
|
510
|
+
: {}),
|
|
408
511
|
// CSS custom properties for animation duration
|
|
409
512
|
...(entrancePreset && !entranceStagger ? {
|
|
410
513
|
"--nav-entrance-duration": `${entranceDuration}ms`,
|
|
@@ -412,21 +515,39 @@ export default function Navbar({
|
|
|
412
515
|
} as React.CSSProperties : {}),
|
|
413
516
|
}}
|
|
414
517
|
>
|
|
518
|
+
{/* Responsive nav CSS custom properties + media queries */}
|
|
519
|
+
{responsiveCSS && (
|
|
520
|
+
<style dangerouslySetInnerHTML={{ __html: responsiveCSS }} />
|
|
521
|
+
)}
|
|
415
522
|
{/* Desktop: 12-column grid constrained to --grid-width */}
|
|
416
523
|
<div
|
|
417
524
|
className="hidden lg:grid"
|
|
418
|
-
style={
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
525
|
+
style={hasResponsive
|
|
526
|
+
? {
|
|
527
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
528
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
529
|
+
marginLeft: "auto",
|
|
530
|
+
marginRight: "auto",
|
|
531
|
+
paddingLeft: "var(--nav-padding-h)",
|
|
532
|
+
paddingRight: "var(--nav-padding-h)",
|
|
533
|
+
paddingTop: "var(--nav-padding-v)",
|
|
534
|
+
paddingBottom: "var(--nav-padding-v)",
|
|
535
|
+
alignItems: "var(--nav-vertical-align)" as React.CSSProperties["alignItems"],
|
|
536
|
+
columnGap: "var(--nav-items-gap)",
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
540
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
541
|
+
marginLeft: "auto",
|
|
542
|
+
marginRight: "auto",
|
|
543
|
+
paddingLeft: `${paddingH}px`,
|
|
544
|
+
paddingRight: `${paddingH}px`,
|
|
545
|
+
paddingTop: `${paddingV}px`,
|
|
546
|
+
paddingBottom: `${paddingV}px`,
|
|
547
|
+
alignItems: gridAlignItems,
|
|
548
|
+
columnGap: `${itemsGap}px`,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
430
551
|
>
|
|
431
552
|
{/* Logo */}
|
|
432
553
|
<div
|
|
@@ -480,7 +601,7 @@ export default function Navbar({
|
|
|
480
601
|
gridColumn: `${item.grid_column} / span ${item.column_span || 1}`,
|
|
481
602
|
gridRow: 1,
|
|
482
603
|
display: "flex",
|
|
483
|
-
justifyContent: itemsJustify,
|
|
604
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
484
605
|
alignItems: "center",
|
|
485
606
|
minWidth: 0,
|
|
486
607
|
overflow: "hidden",
|
|
@@ -518,9 +639,9 @@ export default function Navbar({
|
|
|
518
639
|
style={{
|
|
519
640
|
gridColumn: `${logoCols + 1} / -1`,
|
|
520
641
|
display: "flex",
|
|
521
|
-
justifyContent: itemsJustify,
|
|
642
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
522
643
|
alignItems: "center",
|
|
523
|
-
gap: `${design?.items_gap ?? 32}px`,
|
|
644
|
+
gap: hasResponsive ? "var(--nav-items-gap)" : `${design?.items_gap ?? 32}px`,
|
|
524
645
|
}}
|
|
525
646
|
>
|
|
526
647
|
{unplacedItems.map((item) => (
|
package/lib/builder/constants.ts
CHANGED
|
@@ -57,11 +57,12 @@ export const ADMIN_ERROR_DARK = "#d42f1a";
|
|
|
57
57
|
//
|
|
58
58
|
// BLUE (#076bff) — Columns: outlines, resize handles, drag grip,
|
|
59
59
|
// span badge, column selection/hover chrome.
|
|
60
|
-
//
|
|
60
|
+
// GREEN-B (#0d9668) — Blocks: "+ Add Block" buttons, block toolbar
|
|
61
61
|
// pill, block selection ring, block-level actions.
|
|
62
|
-
//
|
|
62
|
+
// BLUE (#076bff) — Drop zones: gap drop targets during drag,
|
|
63
63
|
// insertion lines, "Drop Here" labels, swap target
|
|
64
|
-
// highlight (
|
|
64
|
+
// highlight (blue border + tinted background).
|
|
65
|
+
// (Merged with column color for coherence.)
|
|
65
66
|
// VIOLET (#8b5cf6) — Custom sections: saved section cards, custom
|
|
66
67
|
// section instance badges, section editor chrome,
|
|
67
68
|
// "Create New" custom section button.
|
|
@@ -72,7 +73,7 @@ export const ADMIN_ERROR_DARK = "#d42f1a";
|
|
|
72
73
|
// while a delete button on a block toolbar is ORANGE.
|
|
73
74
|
|
|
74
75
|
export const BUILDER_BLUE = "#076bff"; // Columns
|
|
75
|
-
export const BUILDER_ORANGE = "#
|
|
76
|
+
export const BUILDER_ORANGE = "#0d9668"; // Blocks (emerald — was orange #e28b00)
|
|
76
77
|
export const BUILDER_GREEN = "#22c55e"; // Drop zones
|
|
77
78
|
export const BUILDER_VIOLET = "#8b5cf6"; // Custom sections
|
|
78
79
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Extracted from serializer.ts in Session 162.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Page, ContentBlock, ContentItem,
|
|
8
|
+
import type { Page, ContentBlock, ContentItem, PageSectionV2, SectionColumn, SectionV2Settings, CustomSectionInstance, ParallaxGroup } from "../../../lib/sanity/types";
|
|
9
9
|
import type { BuilderState, PageSettings } from "../types";
|
|
10
10
|
import { generateKey } from "../utils";
|
|
11
11
|
import { DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR, DEFAULT_GRID_WIDTH } from "../constants";
|
|
@@ -18,39 +18,6 @@ import { migrateProjectGridV1ToV2, normalizeBlockAnimationFields } from "./migra
|
|
|
18
18
|
// Section Normalizers
|
|
19
19
|
// ============================================
|
|
20
20
|
|
|
21
|
-
export function normalizePageSection(section: Partial<PageSection> & { _key: string }): PageSection {
|
|
22
|
-
const ensuredBlock = ensureKeys((section.block || []) as SectionBlock[]) as (SectionBlock & { _key: string })[];
|
|
23
|
-
const ensuredBlockType = ensuredBlock[0]?._type || "projectGridBlock";
|
|
24
|
-
// BUG-022 fix: Preserve the original section_type even if unknown.
|
|
25
|
-
// Only default when the field is missing entirely (new sections).
|
|
26
|
-
const sectionType = section.section_type || (SECTION_TYPE_MAP[ensuredBlockType] || "projectGrid");
|
|
27
|
-
|
|
28
|
-
// Warn on unknown types but don't override — future types should survive round-trips
|
|
29
|
-
if (!Object.values(SECTION_TYPE_MAP).includes(sectionType)) {
|
|
30
|
-
console.warn(`[Serializer] Unknown section_type: ${sectionType} — preserving as-is`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Auto-migrate projectGridBlock v1 → v2
|
|
34
|
-
if (ensuredBlock[0]) {
|
|
35
|
-
migrateProjectGridV1ToV2(ensuredBlock[0] as unknown as Record<string, unknown>);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Session 117: Migrate animation fields on section blocks
|
|
39
|
-
if (ensuredBlock[0]) {
|
|
40
|
-
normalizeBlockAnimationFields(ensuredBlock[0] as unknown as Record<string, unknown>);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
_type: "pageSection",
|
|
45
|
-
_key: section._key,
|
|
46
|
-
section_type: sectionType as import("../../../lib/sanity/types").PageSectionType,
|
|
47
|
-
block: [ensuredBlock[0] || { _type: ensuredBlockType, _key: generateKey() }] as [SectionBlock],
|
|
48
|
-
settings: section.settings || {},
|
|
49
|
-
// BUG-013 fix: preserve responsive overrides for sections
|
|
50
|
-
...(section.responsive ? { responsive: section.responsive } : {}),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
21
|
/**
|
|
55
22
|
* Normalize a PageSectionV2 from Sanity data into the builder's shape.
|
|
56
23
|
* Ensures all columns have _keys, validates grid positions, and fills defaults.
|
|
@@ -177,14 +144,9 @@ export function normalizeParallaxGroup(group: Partial<ParallaxGroup> & { _key: s
|
|
|
177
144
|
}
|
|
178
145
|
|
|
179
146
|
/**
|
|
180
|
-
* Normalize a content item (
|
|
147
|
+
* Normalize a content item (PageSectionV2, ParallaxGroup, CustomSectionInstance, etc.).
|
|
181
148
|
*/
|
|
182
149
|
export function migrateContentItem(item: Record<string, unknown>): ContentItem {
|
|
183
|
-
// PageSection — normalize and return
|
|
184
|
-
if (item._type === "pageSection") {
|
|
185
|
-
return normalizePageSection(item as unknown as Partial<PageSection> & { _key: string });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
150
|
// PageSectionV2 — normalize and return
|
|
189
151
|
if (item._type === "pageSectionV2") {
|
|
190
152
|
return normalizePageSectionV2(item as unknown as Partial<PageSectionV2> & { _key: string });
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Extracted from serializer.ts in Session 162.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ContentBlock, ContentItem,
|
|
9
|
-
import {
|
|
8
|
+
import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup } from "../../../lib/sanity/types";
|
|
9
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../lib/sanity/types";
|
|
10
10
|
import type { BuilderState } from "../types";
|
|
11
11
|
import { DEFAULT_BG_COLOR, DEFAULT_TEXT_COLOR } from "../constants";
|
|
12
12
|
|
|
@@ -67,74 +67,6 @@ function serializeBlock(block: ContentBlock): ContentBlock {
|
|
|
67
67
|
return serialized;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// ============================================
|
|
71
|
-
// Section Serialization
|
|
72
|
-
// ============================================
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Normalize section responsive overrides for serialization (save path).
|
|
76
|
-
* Strips undefined/null/empty string values and removes empty viewport objects.
|
|
77
|
-
*/
|
|
78
|
-
function normalizeSectionResponsiveForSerialize(
|
|
79
|
-
responsive: PageSection["responsive"] | undefined
|
|
80
|
-
): PageSection["responsive"] | undefined {
|
|
81
|
-
if (!responsive) return undefined;
|
|
82
|
-
|
|
83
|
-
const result: NonNullable<PageSection["responsive"]> = {};
|
|
84
|
-
|
|
85
|
-
for (const vp of ["tablet", "phone"] as const) {
|
|
86
|
-
const overrides = responsive[vp];
|
|
87
|
-
if (!overrides || typeof overrides !== "object") continue;
|
|
88
|
-
|
|
89
|
-
const cleaned: Record<string, unknown> = {};
|
|
90
|
-
for (const [key, val] of Object.entries(overrides)) {
|
|
91
|
-
if (val === undefined || val === null) continue;
|
|
92
|
-
if (typeof val === "string" && val.trim() === "") continue;
|
|
93
|
-
cleaned[key] = val;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (Object.keys(cleaned).length > 0) {
|
|
97
|
-
result[vp] = cleaned as typeof result[typeof vp];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function serializePageSection(section: PageSection): Record<string, unknown> {
|
|
105
|
-
const s = section.settings;
|
|
106
|
-
return {
|
|
107
|
-
_key: section._key,
|
|
108
|
-
_type: "pageSection",
|
|
109
|
-
section_type: section.section_type,
|
|
110
|
-
block: (section.block || []).map((b) => serializeBlock(b as ContentBlock)),
|
|
111
|
-
// BUG-013 fix: persist responsive overrides for sections (normalized: strips empty values)
|
|
112
|
-
responsive: normalizeSectionResponsiveForSerialize(section.responsive),
|
|
113
|
-
settings: s ? stripUndefined({
|
|
114
|
-
background_color: s.background_color,
|
|
115
|
-
background_opacity: s.background_opacity,
|
|
116
|
-
background_image: s.background_image,
|
|
117
|
-
background_size: s.background_size,
|
|
118
|
-
background_position: s.background_position,
|
|
119
|
-
background_repeat: s.background_repeat,
|
|
120
|
-
spacing_top: s.spacing_top,
|
|
121
|
-
spacing_right: s.spacing_right,
|
|
122
|
-
spacing_bottom: s.spacing_bottom,
|
|
123
|
-
spacing_left: s.spacing_left,
|
|
124
|
-
offset_top: s.offset_top,
|
|
125
|
-
offset_right: s.offset_right,
|
|
126
|
-
offset_bottom: s.offset_bottom,
|
|
127
|
-
offset_left: s.offset_left,
|
|
128
|
-
border_color: s.border_color,
|
|
129
|
-
border_width: s.border_width,
|
|
130
|
-
border_style: s.border_style,
|
|
131
|
-
border_sides: s.border_sides,
|
|
132
|
-
border_radius: s.border_radius,
|
|
133
|
-
enter_animation: s.enter_animation,
|
|
134
|
-
}) : undefined,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
70
|
/**
|
|
139
71
|
* Normalize V2 section responsive overrides for serialization.
|
|
140
72
|
*/
|
|
@@ -283,12 +215,9 @@ function serializeParallaxGroup(group: ParallaxGroup): Record<string, unknown> {
|
|
|
283
215
|
// ============================================
|
|
284
216
|
|
|
285
217
|
/**
|
|
286
|
-
* Serialize a content item
|
|
218
|
+
* Serialize a content item for Sanity.
|
|
287
219
|
*/
|
|
288
220
|
function serializeContentItem(item: ContentItem): Record<string, unknown> {
|
|
289
|
-
if (isPageSection(item)) {
|
|
290
|
-
return serializePageSection(item);
|
|
291
|
-
}
|
|
292
221
|
if (isPageSectionV2(item)) {
|
|
293
222
|
return serializePageSectionV2(item);
|
|
294
223
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BuilderStore, BuilderState } from "./types";
|
|
2
|
-
import type { ContentBlock, ContentItem,
|
|
3
|
-
import {
|
|
2
|
+
import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup } from "../../lib/sanity/types";
|
|
3
|
+
import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
|
|
4
4
|
import { createDefaultBlock } from "./defaults";
|
|
5
5
|
import { generateKey } from "./utils";
|
|
6
6
|
import { findSectionPath, updateSectionAtPath, moveBlockInState } from "./store-helpers";
|
|
@@ -27,13 +27,6 @@ function applyBlockUpdate(
|
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
return rows.map((item) => {
|
|
30
|
-
if (isPageSection(item)) {
|
|
31
|
-
const block = item.block[0];
|
|
32
|
-
if (block && block._key === blockKey) {
|
|
33
|
-
return { ...item, block: [{ ...block, ...updates } as SectionBlock] };
|
|
34
|
-
}
|
|
35
|
-
return item;
|
|
36
|
-
}
|
|
37
30
|
if (isPageSectionV2(item)) {
|
|
38
31
|
return { ...item, columns: updateColumns(item.columns) };
|
|
39
32
|
}
|
|
@@ -68,17 +61,8 @@ export function createBlockActions(set: StoreSet, get: StoreGet) {
|
|
|
68
61
|
cols.map((c) => ({ ...c, blocks: c.blocks.filter((b) => b._key !== blockKey) }));
|
|
69
62
|
|
|
70
63
|
set((state) => {
|
|
71
|
-
// Handle PageSection: delete the entire section if its block is being deleted
|
|
72
|
-
const updatedRows = state.rows.filter((item) => {
|
|
73
|
-
if (isPageSection(item)) {
|
|
74
|
-
const block = item.block[0];
|
|
75
|
-
return !(block && block._key === blockKey);
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
64
|
// Handle V2 Sections + ParallaxGroup slides
|
|
81
|
-
const finalRows =
|
|
65
|
+
const finalRows = state.rows.map((item) => {
|
|
82
66
|
if (isPageSectionV2(item)) {
|
|
83
67
|
return { ...item, columns: filterBlocks(item.columns) } as ContentItem;
|
|
84
68
|
}
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
SectionColumn,
|
|
17
17
|
SectionV2Preset,
|
|
18
18
|
} from "../../lib/sanity/types";
|
|
19
|
-
import {
|
|
19
|
+
import { isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
|
|
20
20
|
import { columnsFromPreset, detectPreset } from "./cascade";
|
|
21
21
|
import { resizeColumnLeft as cascadeResizeLeft, moveColumn as cascadeMoveColumn, type ResizeLeftResult } from "./cascade";
|
|
22
22
|
import { applyBlocksToColumns, toCascadeColumns, type CascadeColumn } from "./cascade-helpers";
|
|
@@ -62,7 +62,7 @@ export function moveBlockInState(
|
|
|
62
62
|
|
|
63
63
|
// Pass 1: find and remove block from its current section (V2 + parallax slides)
|
|
64
64
|
// RC-006 fix: Early-exit once block is found, consistent with selectBlock()
|
|
65
|
-
// search pattern.
|
|
65
|
+
// search pattern.
|
|
66
66
|
let rowsAfterRemove = rows;
|
|
67
67
|
for (let i = 0; i < rows.length; i++) {
|
|
68
68
|
const item = rows[i];
|
|
@@ -2,11 +2,8 @@ import type { BuilderStore, BuilderState, BlockType } from "./types";
|
|
|
2
2
|
import type {
|
|
3
3
|
ContentBlock,
|
|
4
4
|
ContentItem,
|
|
5
|
-
PageSection,
|
|
6
5
|
PageSectionV2,
|
|
7
6
|
SectionV2Settings,
|
|
8
|
-
SectionSettings,
|
|
9
|
-
SectionBlock,
|
|
10
7
|
CustomSectionInstance,
|
|
11
8
|
ParallaxGroup,
|
|
12
9
|
ParallaxSlideV2,
|
|
@@ -14,7 +11,6 @@ import type {
|
|
|
14
11
|
EnterAnimationConfig,
|
|
15
12
|
} from "../../lib/sanity/types";
|
|
16
13
|
import {
|
|
17
|
-
isPageSection,
|
|
18
14
|
isPageSectionV2,
|
|
19
15
|
isCustomSectionInstance,
|
|
20
16
|
isParallaxGroup,
|
|
@@ -52,20 +48,36 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
|
52
48
|
// ---- Section operations ----
|
|
53
49
|
|
|
54
50
|
/**
|
|
55
|
-
* Add a
|
|
56
|
-
*
|
|
57
|
-
*
|
|
51
|
+
* Add a Project Grid section — creates a PageSectionV2 with a single
|
|
52
|
+
* full-width column (span 12) containing a pre-populated projectGridBlock.
|
|
53
|
+
*
|
|
54
|
+
* Session 164: Migrated from V1 (PageSection) to V2 (PageSectionV2).
|
|
55
|
+
* Existing V1 pages still work — V1 update/delete actions are kept until
|
|
56
|
+
* the data migration in Session 165.
|
|
58
57
|
*/
|
|
59
58
|
addSection: (blockType: "projectGridBlock", afterRowKey?: string | null): void => {
|
|
60
59
|
get()._pushSnapshot();
|
|
61
|
-
const block = createDefaultBlock(blockType)
|
|
62
|
-
const
|
|
63
|
-
|
|
60
|
+
const block = createDefaultBlock(blockType);
|
|
61
|
+
const gridColumns = 12;
|
|
62
|
+
|
|
63
|
+
const newSection: PageSectionV2 = {
|
|
64
|
+
_type: "pageSectionV2",
|
|
64
65
|
_key: generateKey(),
|
|
65
|
-
section_type: "
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
section_type: "empty-v2",
|
|
67
|
+
columns: [
|
|
68
|
+
{
|
|
69
|
+
_key: generateKey(),
|
|
70
|
+
grid_column: 1,
|
|
71
|
+
grid_row: 1,
|
|
72
|
+
span: gridColumns,
|
|
73
|
+
blocks: [block],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
68
76
|
settings: {
|
|
77
|
+
preset: "full",
|
|
78
|
+
grid_columns: gridColumns,
|
|
79
|
+
col_gap: 20,
|
|
80
|
+
row_gap: 20,
|
|
69
81
|
spacing_top: "32",
|
|
70
82
|
spacing_bottom: "32",
|
|
71
83
|
},
|
|
@@ -97,50 +109,6 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
|
97
109
|
});
|
|
98
110
|
},
|
|
99
111
|
|
|
100
|
-
updateSectionSettings: (
|
|
101
|
-
sectionKey: string,
|
|
102
|
-
settings: Partial<SectionSettings>
|
|
103
|
-
): void => {
|
|
104
|
-
set((state) => ({
|
|
105
|
-
rows: state.rows.map((item) =>
|
|
106
|
-
item._key === sectionKey && isPageSection(item)
|
|
107
|
-
? { ...item, settings: { ...item.settings, ...settings } }
|
|
108
|
-
: item
|
|
109
|
-
),
|
|
110
|
-
isDirty: true,
|
|
111
|
-
}));
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
/** BUG-013 fix: Update responsive overrides for a PageSection */
|
|
115
|
-
updateSectionResponsive: (
|
|
116
|
-
sectionKey: string,
|
|
117
|
-
responsive: PageSection["responsive"]
|
|
118
|
-
): void => {
|
|
119
|
-
set((state) => ({
|
|
120
|
-
rows: state.rows.map((item) =>
|
|
121
|
-
item._key === sectionKey && isPageSection(item)
|
|
122
|
-
? { ...item, responsive }
|
|
123
|
-
: item
|
|
124
|
-
),
|
|
125
|
-
isDirty: true,
|
|
126
|
-
}));
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
updateSectionBlock: (sectionKey: string, updates: Partial<ContentBlock>): void => {
|
|
130
|
-
set((state) => ({
|
|
131
|
-
rows: state.rows.map((item) => {
|
|
132
|
-
if (item._key !== sectionKey || !isPageSection(item)) return item;
|
|
133
|
-
const block = item.block[0];
|
|
134
|
-
if (!block) return item;
|
|
135
|
-
return {
|
|
136
|
-
...item,
|
|
137
|
-
block: [{ ...block, ...updates } as SectionBlock],
|
|
138
|
-
};
|
|
139
|
-
}),
|
|
140
|
-
isDirty: true,
|
|
141
|
-
}));
|
|
142
|
-
},
|
|
143
|
-
|
|
144
112
|
deleteSection: (sectionKey: string): void => {
|
|
145
113
|
get()._pushSnapshot();
|
|
146
114
|
set((state) => ({
|
|
@@ -165,13 +133,7 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
|
165
133
|
const clone = JSON.parse(JSON.stringify(original)) as ContentItem;
|
|
166
134
|
clone._key = generateKey();
|
|
167
135
|
// Re-key internal structures
|
|
168
|
-
if (
|
|
169
|
-
if (clone.block.length > 0) {
|
|
170
|
-
clone.block = [
|
|
171
|
-
{ ...clone.block[0], _key: generateKey() },
|
|
172
|
-
] as [SectionBlock];
|
|
173
|
-
}
|
|
174
|
-
} else if (isPageSectionV2(clone)) {
|
|
136
|
+
if (isPageSectionV2(clone)) {
|
|
175
137
|
(clone as PageSectionV2).columns = (clone as PageSectionV2).columns.map(
|
|
176
138
|
(col) => ({
|
|
177
139
|
...col,
|
package/lib/builder/store.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { BuilderStore, BuilderState, BlockType, PageSettings, CanvasTool, D
|
|
|
3
3
|
import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
|
|
4
4
|
import { stateToDocument, documentToState } from "./serializer";
|
|
5
5
|
import { generateKey } from "./utils";
|
|
6
|
-
import type { ContentBlock, ContentItem,
|
|
7
|
-
import {
|
|
6
|
+
import type { ContentBlock, ContentItem, PageSectionV2, SectionV2Settings, PageMetadata, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
|
|
7
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
8
8
|
import { createDefaultBlock, createDefaultParallaxSlide } from "./defaults";
|
|
9
9
|
import { MAX_HISTORY, pushSnapshot } from "./history";
|
|
10
10
|
import {
|
|
@@ -48,10 +48,7 @@ function getBlockParentCache(rows: ContentItem[]): Map<string, BlockParent> {
|
|
|
48
48
|
|
|
49
49
|
const cache = new Map<string, BlockParent>();
|
|
50
50
|
for (const item of rows) {
|
|
51
|
-
if (
|
|
52
|
-
const sBlock = Array.isArray(item.block) ? item.block[0] : undefined;
|
|
53
|
-
if (sBlock) cache.set(sBlock._key, { rowKey: item._key, colKey: null });
|
|
54
|
-
} else if (isPageSectionV2(item)) {
|
|
51
|
+
if (isPageSectionV2(item)) {
|
|
55
52
|
const v2 = item as PageSectionV2;
|
|
56
53
|
for (const col of v2.columns) {
|
|
57
54
|
for (const b of col.blocks) {
|