@morphika/andami 0.1.10 → 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/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/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/index.ts +0 -1
- package/components/builder/settings-panel/responsive-helpers.ts +2 -50
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
- 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 +2 -70
- package/package.json +2 -2
- 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 +7 -5
- package/components/blocks/SectionRenderer.tsx +0 -171
- package/components/builder/settings-panel/LayoutTab.tsx +0 -346
- package/sanity/schemas/pageSection.ts +0 -157
|
@@ -109,7 +109,7 @@ export default function SortableBlock({
|
|
|
109
109
|
style={style}
|
|
110
110
|
className={`relative transition-[opacity,box-shadow] ${
|
|
111
111
|
isDragging
|
|
112
|
-
? "ring-2 ring-[#
|
|
112
|
+
? "ring-2 ring-[#0d9668] ring-offset-1 ring-offset-transparent rounded"
|
|
113
113
|
: ""
|
|
114
114
|
}`}
|
|
115
115
|
onClick={(e) => {
|
|
@@ -129,7 +129,7 @@ export default function SortableBlock({
|
|
|
129
129
|
isSelected
|
|
130
130
|
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${BUILDER_ORANGE}` }
|
|
131
131
|
: isHovered
|
|
132
|
-
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(
|
|
132
|
+
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(13, 150, 104, 0.4)` }
|
|
133
133
|
: undefined
|
|
134
134
|
}
|
|
135
135
|
/>
|
|
@@ -149,7 +149,7 @@ export default function SortableBlock({
|
|
|
149
149
|
className="flex items-center gap-1.5"
|
|
150
150
|
style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "top center" }}
|
|
151
151
|
>
|
|
152
|
-
<div className="flex items-center bg-[#
|
|
152
|
+
<div className="flex items-center bg-[#0d9668] rounded shadow-lg overflow-hidden">
|
|
153
153
|
{/* Move up arrow */}
|
|
154
154
|
<button
|
|
155
155
|
onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
|
|
@@ -8,11 +8,11 @@ import { useBuilderStore } from "../../lib/builder/store";
|
|
|
8
8
|
import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
|
|
9
9
|
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
|
-
import type { ContentItem,
|
|
12
|
-
import {
|
|
11
|
+
import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
|
|
12
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
13
13
|
import { getRowLayoutStyles } from "../../lib/builder/layout-styles";
|
|
14
14
|
import { normalizeMinHeight } from "../../lib/builder/utils";
|
|
15
|
-
import { getSectionV2SettingValue
|
|
15
|
+
import { getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Convert vh-based CSS values to pixels using the simulated device viewport height.
|
|
@@ -29,14 +29,8 @@ function vhToBuilderPx(value: string | undefined, deviceHeight: number): string
|
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Section type labels */
|
|
33
|
-
const SECTION_TYPE_LABELS: Record<string, string> = {
|
|
34
|
-
projectGrid: "Project Grid",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
32
|
/**
|
|
38
33
|
* Get the section label for a content item.
|
|
39
|
-
* For PageSections: uses section_type directly (no structural inference).
|
|
40
34
|
* For PageSectionV2: returns "Section".
|
|
41
35
|
*/
|
|
42
36
|
function getSectionLabel(item: ContentItem): string | null {
|
|
@@ -49,9 +43,6 @@ function getSectionLabel(item: ContentItem): string | null {
|
|
|
49
43
|
if (isPageSectionV2(item)) {
|
|
50
44
|
return "Section";
|
|
51
45
|
}
|
|
52
|
-
if (isPageSection(item)) {
|
|
53
|
-
return SECTION_TYPE_LABELS[item.section_type] || "Section";
|
|
54
|
-
}
|
|
55
46
|
return null;
|
|
56
47
|
}
|
|
57
48
|
|
|
@@ -108,9 +99,8 @@ export default function SortableRow({
|
|
|
108
99
|
zIndex: isDragging ? 50 : undefined,
|
|
109
100
|
};
|
|
110
101
|
|
|
111
|
-
// Determine if this is a
|
|
102
|
+
// Determine if this is a PageSectionV2
|
|
112
103
|
const isV2Section = isPageSectionV2(row);
|
|
113
|
-
const isSection = isPageSection(row);
|
|
114
104
|
const sectionLabel = getSectionLabel(row);
|
|
115
105
|
|
|
116
106
|
// For sections: use section settings — viewport-aware for both V1 and V2 sections
|
|
@@ -133,17 +123,6 @@ export default function SortableRow({
|
|
|
133
123
|
return merged;
|
|
134
124
|
}
|
|
135
125
|
|
|
136
|
-
if (isSection) {
|
|
137
|
-
const ps = row as PageSection;
|
|
138
|
-
const base = ps.settings || {};
|
|
139
|
-
// BUG-013 continuation: Merge responsive overrides for V1 PageSections too
|
|
140
|
-
const merged: Record<string, unknown> = { ...base };
|
|
141
|
-
for (const key of overridableKeys) {
|
|
142
|
-
merged[key] = getRowSettingValue(ps, activeViewport, key, (base as Record<string, unknown>)[key]);
|
|
143
|
-
}
|
|
144
|
-
return merged;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
126
|
// CustomSectionInstance: merge base section settings from cache + per-instance overrides
|
|
148
127
|
// Then apply viewport-aware responsive resolution (same pattern as V2 sections)
|
|
149
128
|
if (isCustomSectionInstance(row)) {
|
|
@@ -169,7 +148,7 @@ export default function SortableRow({
|
|
|
169
148
|
}
|
|
170
149
|
|
|
171
150
|
return {};
|
|
172
|
-
}, [isV2Section,
|
|
151
|
+
}, [isV2Section, row, activeViewport, customSectionCache]);
|
|
173
152
|
|
|
174
153
|
const bgColor = (resolvedSettings as Record<string, unknown>).background_color as string || "transparent";
|
|
175
154
|
|
|
@@ -187,7 +166,7 @@ export default function SortableRow({
|
|
|
187
166
|
const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown> || {});
|
|
188
167
|
|
|
189
168
|
const showToolbar = isSelected || isHovered;
|
|
190
|
-
const coverRow =
|
|
169
|
+
const coverRow = false;
|
|
191
170
|
|
|
192
171
|
// ---- Preview Mode: clean rendering with row styles applied ----
|
|
193
172
|
if (previewMode) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { useState, useCallback, useRef } from "react";
|
|
12
12
|
import { ProjectGridCard } from "./shared";
|
|
13
13
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
14
|
-
import { ADMIN_BLUE,
|
|
14
|
+
import { ADMIN_BLUE, DROP_BLUE, CrossArrowIcon } from "./drag-utils";
|
|
15
15
|
import type { ProjectGridItem } from "../../../lib/sanity/types";
|
|
16
16
|
|
|
17
17
|
// ─── Props ───────────────────────────────────────────────────────────
|
|
@@ -278,11 +278,11 @@ export default function ProjectCardWrapper({
|
|
|
278
278
|
style={{
|
|
279
279
|
position: "absolute",
|
|
280
280
|
inset: 0,
|
|
281
|
-
backgroundColor: "rgba(
|
|
281
|
+
backgroundColor: "rgba(7,107,255,0.30)",
|
|
282
282
|
borderRadius: br,
|
|
283
283
|
pointerEvents: "none",
|
|
284
284
|
zIndex: 5,
|
|
285
|
-
border: `2px solid ${
|
|
285
|
+
border: `2px solid ${DROP_BLUE}`,
|
|
286
286
|
}}
|
|
287
287
|
/>
|
|
288
288
|
)}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Session 162: Extracted from LiveProjectGridPreview.tsx (Phase B1).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { BUILDER_BLUE
|
|
10
|
+
import { BUILDER_BLUE } from "../../../lib/builder/constants";
|
|
11
11
|
import type { MasonryOutput } from "../../../lib/builder/masonry";
|
|
12
12
|
|
|
13
13
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
@@ -16,7 +16,7 @@ export const HOLD_DELAY = 150; // ms before card-body drag activates
|
|
|
16
16
|
export const MOVE_THRESHOLD_SQ = 9; // 3px² — grabbed → dragging
|
|
17
17
|
export const CANCEL_DURATION = 200; // ms — cancel fly-back animation
|
|
18
18
|
export const ADMIN_BLUE = BUILDER_BLUE;
|
|
19
|
-
export const
|
|
19
|
+
export const DROP_BLUE = BUILDER_BLUE;
|
|
20
20
|
|
|
21
21
|
// ─── Types ───────────────────────────────────────────────────────────
|
|
22
22
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
10
|
-
import type { ContentBlock,
|
|
10
|
+
import type { ContentBlock, ProjectGridBlock } from "../../../lib/sanity/types";
|
|
11
11
|
import type { HoverEffectConfig } from "../../../lib/animation/hover-effect-types";
|
|
12
12
|
import EnterAnimationPicker from "../editors/EnterAnimationPicker";
|
|
13
13
|
import HoverEffectPicker from "../editors/HoverEffectPicker";
|
|
@@ -32,25 +32,11 @@ export function getBlockHoverEffect(block: ContentBlock): HoverEffectConfig | un
|
|
|
32
32
|
|
|
33
33
|
interface AnimationTabProps {
|
|
34
34
|
selectedBlock: { block: ContentBlock; rowKey: string; colKey: string; isSection: boolean } | null;
|
|
35
|
-
selectedSection: PageSection | null;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export function AnimationTab({ selectedBlock
|
|
37
|
+
export function AnimationTab({ selectedBlock }: AnimationTabProps) {
|
|
39
38
|
const store = useBuilderStore();
|
|
40
39
|
|
|
41
|
-
// PageSection (V1): enter animation on the section settings level
|
|
42
|
-
if (selectedSection) {
|
|
43
|
-
return (
|
|
44
|
-
<EnterAnimationPicker
|
|
45
|
-
mode={{ level: "section", parentConfig: store.pageSettings.enter_animation }}
|
|
46
|
-
config={selectedSection.settings?.enter_animation}
|
|
47
|
-
onChange={(cfg) => {
|
|
48
|
-
store.updateSectionSettings(selectedSection._key, { enter_animation: cfg });
|
|
49
|
-
}}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
40
|
// Block level: type-specific enter picker + card entrance for projectGrid
|
|
55
41
|
if (selectedBlock) {
|
|
56
42
|
const isProjectGrid = selectedBlock.block._type === "projectGridBlock";
|
|
@@ -1,60 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Responsive helpers for
|
|
2
|
+
* Responsive helpers for setting resolution.
|
|
3
3
|
*
|
|
4
4
|
* Session 64: Extracted from SettingsPanel.tsx.
|
|
5
|
-
* Session
|
|
6
|
-
* compatible with PageSection (settings + responsive shape).
|
|
5
|
+
* Session 166: Removed V1 PageSection helpers (getRowSettingValue, hasRowSettingOverride, setRowResponsiveOverride).
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import type { PageSection } from "../../../lib/sanity/types";
|
|
10
8
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
* Generic shape for items with settings + responsive overrides.
|
|
14
|
-
* Compatible with PageSection (and formerly Row).
|
|
15
|
-
* Uses `unknown` to accept any settings interface without index signature issues.
|
|
16
|
-
*/
|
|
17
|
-
type SettingsItem = {
|
|
18
|
-
settings?: unknown;
|
|
19
|
-
responsive?: Record<string, Record<string, unknown>>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/** Get effective value for a section setting, checking viewport overrides */
|
|
23
|
-
export function getRowSettingValue<T>(item: SettingsItem, viewport: DeviceViewport, property: string, fallback: T): T {
|
|
24
|
-
if (viewport === "desktop") {
|
|
25
|
-
const val = (item.settings as Record<string, unknown> || {})[property];
|
|
26
|
-
return (val !== undefined ? val : fallback) as T;
|
|
27
|
-
}
|
|
28
|
-
const override = item.responsive?.[viewport]?.[property];
|
|
29
|
-
if (override !== undefined) return override as T;
|
|
30
|
-
const base = (item.settings as Record<string, unknown> || {})[property];
|
|
31
|
-
return (base !== undefined ? base : fallback) as T;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Check if a section setting has a responsive override */
|
|
35
|
-
export function hasRowSettingOverride(item: SettingsItem, viewport: DeviceViewport, property: string): boolean {
|
|
36
|
-
if (viewport === "desktop") return false;
|
|
37
|
-
return item.responsive?.[viewport]?.[property] !== undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Set a responsive override on a section setting, returning the updated fields */
|
|
41
|
-
export function setRowResponsiveOverride(item: SettingsItem, viewport: DeviceViewport, property: string, value: unknown): Partial<PageSection> {
|
|
42
|
-
if (viewport === "desktop") {
|
|
43
|
-
return { settings: { ...(item.settings as Record<string, unknown> || {}), [property]: value } } as Partial<PageSection>;
|
|
44
|
-
}
|
|
45
|
-
const existing = item.responsive || {};
|
|
46
|
-
const vp = viewport as "tablet" | "phone";
|
|
47
|
-
const vpOverrides = { ...(existing[vp] || {}), [property]: value };
|
|
48
|
-
if (value === undefined) {
|
|
49
|
-
delete (vpOverrides as Record<string, unknown>)[property];
|
|
50
|
-
}
|
|
51
|
-
const responsive = { ...existing, [vp]: vpOverrides };
|
|
52
|
-
if (Object.keys(vpOverrides).length === 0) {
|
|
53
|
-
delete responsive[vp];
|
|
54
|
-
}
|
|
55
|
-
return { responsive: Object.keys(responsive).length ? responsive : undefined } as Partial<PageSection>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
10
|
// ============================================
|
|
59
11
|
// Block Layout responsive helpers
|
|
60
12
|
// ============================================
|
|
@@ -11,7 +11,6 @@ import { BLOCK_GRADIENTS, BLOCK_ICON_COMPONENTS } from "../blockStyles";
|
|
|
11
11
|
import type {
|
|
12
12
|
ContentBlock,
|
|
13
13
|
ContentItem,
|
|
14
|
-
PageSection,
|
|
15
14
|
PageSectionV2,
|
|
16
15
|
CustomSectionInstance,
|
|
17
16
|
ParallaxGroup,
|
|
@@ -19,7 +18,6 @@ import type {
|
|
|
19
18
|
SectionColumn,
|
|
20
19
|
} from "../../../lib/sanity/types";
|
|
21
20
|
import {
|
|
22
|
-
isPageSection,
|
|
23
21
|
isPageSectionV2,
|
|
24
22
|
isCustomSectionInstance,
|
|
25
23
|
isParallaxGroup,
|
|
@@ -43,7 +41,6 @@ export function useSettingsPanelSelection() {
|
|
|
43
41
|
|
|
44
42
|
// Find selected elements — handle page sections, V2 sections, and parallax groups/slides
|
|
45
43
|
const selectedItem: ContentItem | undefined = store.rows.find((r) => r._key === store.selectedRowKey);
|
|
46
|
-
const selectedSection: PageSection | null = selectedItem && isPageSection(selectedItem) ? selectedItem : null;
|
|
47
44
|
const selectedSectionV2: PageSectionV2 | null = selectedItem && isPageSectionV2(selectedItem) ? selectedItem : null;
|
|
48
45
|
const selectedCustomSectionInstance: CustomSectionInstance | null = selectedItem && isCustomSectionInstance(selectedItem) ? selectedItem as CustomSectionInstance : null;
|
|
49
46
|
|
|
@@ -76,15 +73,8 @@ export function useSettingsPanelSelection() {
|
|
|
76
73
|
? effectiveSectionV2.columns.find((c) => c._key === store.selectedColumnKey) || null
|
|
77
74
|
: null;
|
|
78
75
|
|
|
79
|
-
// For PageSections, the "block" is section.block[0] — selected automatically
|
|
80
76
|
const selectedBlock: SelectedBlockInfo | null = (() => {
|
|
81
|
-
//
|
|
82
|
-
if (selectedSection) {
|
|
83
|
-
const block = selectedSection.block[0];
|
|
84
|
-
if (block) return { block, rowKey: selectedSection._key, colKey: "", isSection: true };
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
// Regular block search inside rows, V2 sections, and parallax slides
|
|
77
|
+
// Block search inside V2 sections and parallax slides
|
|
88
78
|
if (!store.selectedBlockKey) return null;
|
|
89
79
|
for (const item of store.rows) {
|
|
90
80
|
// V2 sections: search inside columns
|
|
@@ -130,8 +120,6 @@ export function useSettingsPanelSelection() {
|
|
|
130
120
|
? (selectedCustomSectionInstance.custom_section_title || "Saved Section")
|
|
131
121
|
: selectedSectionV2
|
|
132
122
|
? "Section"
|
|
133
|
-
: selectedSection
|
|
134
|
-
? (selectedSection.section_type === "projectGrid" ? "Project Grid" : "Parallax Section")
|
|
135
123
|
: "Page";
|
|
136
124
|
|
|
137
125
|
// Resolve gradient + icon component for the header
|
|
@@ -145,8 +133,6 @@ export function useSettingsPanelSelection() {
|
|
|
145
133
|
? "customSectionInstance"
|
|
146
134
|
: selectedSectionV2
|
|
147
135
|
? "row"
|
|
148
|
-
: selectedSection
|
|
149
|
-
? (selectedSection.block[0]?._type || "row")
|
|
150
136
|
: "page";
|
|
151
137
|
const headerGradient = BLOCK_GRADIENTS[headerStyleKey] || BLOCK_GRADIENTS.page;
|
|
152
138
|
const HeaderIconComponent = BLOCK_ICON_COMPONENTS[headerStyleKey];
|
|
@@ -163,7 +149,6 @@ export function useSettingsPanelSelection() {
|
|
|
163
149
|
|
|
164
150
|
return {
|
|
165
151
|
selectedItem,
|
|
166
|
-
selectedSection,
|
|
167
152
|
selectedSectionV2,
|
|
168
153
|
selectedCustomSectionInstance,
|
|
169
154
|
selectedParallaxGroup,
|
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];
|