@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
|
@@ -31,6 +31,12 @@ interface EnterAnimationWrapperProps {
|
|
|
31
31
|
staggerIndex?: number;
|
|
32
32
|
/** Stagger delay in ms (multiplied by staggerIndex) */
|
|
33
33
|
staggerDelay?: number;
|
|
34
|
+
/**
|
|
35
|
+
* When true, the animation is triggered immediately by the parent
|
|
36
|
+
* instead of using a per-element IntersectionObserver.
|
|
37
|
+
* Used by ProjectGridBlock to fire all card animations at once.
|
|
38
|
+
*/
|
|
39
|
+
forceEnter?: boolean;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -54,6 +60,7 @@ export default function EnterAnimationWrapper({
|
|
|
54
60
|
style,
|
|
55
61
|
staggerIndex,
|
|
56
62
|
staggerDelay,
|
|
63
|
+
forceEnter,
|
|
57
64
|
}: EnterAnimationWrapperProps) {
|
|
58
65
|
const { preset, duration, delay, easing } = config;
|
|
59
66
|
|
|
@@ -66,8 +73,18 @@ export default function EnterAnimationWrapper({
|
|
|
66
73
|
// Get keyframe data for initial hidden state
|
|
67
74
|
const keyframes = getEnterKeyframes(preset);
|
|
68
75
|
|
|
69
|
-
// ──
|
|
76
|
+
// ── forceEnter: parent controls when animation fires ──
|
|
70
77
|
useEffect(() => {
|
|
78
|
+
if (forceEnter && !hasEntered) {
|
|
79
|
+
setHasEntered(true);
|
|
80
|
+
}
|
|
81
|
+
}, [forceEnter, hasEntered]);
|
|
82
|
+
|
|
83
|
+
// ── IntersectionObserver: self-managed trigger (used when forceEnter is not set) ──
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
// Skip per-element observer when parent controls the trigger
|
|
86
|
+
if (forceEnter !== undefined) return;
|
|
87
|
+
|
|
71
88
|
const el = ref.current;
|
|
72
89
|
if (!el || hasEntered) return;
|
|
73
90
|
|
|
@@ -80,8 +97,6 @@ export default function EnterAnimationWrapper({
|
|
|
80
97
|
const observer = new IntersectionObserver(
|
|
81
98
|
([entry]) => {
|
|
82
99
|
if (entry.isIntersecting) {
|
|
83
|
-
// Whether above-the-fold (already visible on mount) or below-the-fold
|
|
84
|
-
// (scrolled into view), behavior is the same: play animation once.
|
|
85
100
|
setHasEntered(true);
|
|
86
101
|
observer.disconnect();
|
|
87
102
|
}
|
|
@@ -91,7 +106,7 @@ export default function EnterAnimationWrapper({
|
|
|
91
106
|
|
|
92
107
|
observer.observe(el);
|
|
93
108
|
return () => observer.disconnect();
|
|
94
|
-
}, [hasEntered]);
|
|
109
|
+
}, [hasEntered, forceEnter]);
|
|
95
110
|
|
|
96
111
|
// If preset is "none" or has no keyframes (typewriter), just render children
|
|
97
112
|
if (preset === "none" || !keyframes) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Page, ContentBlock, ContentItem,
|
|
2
|
-
import {
|
|
3
|
-
import SectionRenderer from "./SectionRenderer";
|
|
1
|
+
import type { Page, ContentBlock, ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
|
|
2
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
4
3
|
import SectionV2Renderer from "./SectionV2Renderer";
|
|
5
4
|
import CustomSectionInstanceRenderer from "./CustomSectionInstanceRenderer";
|
|
6
5
|
import ParallaxGroupRenderer from "./ParallaxGroupRenderer";
|
|
@@ -34,16 +33,6 @@ function getBlockImagePath(block: ContentBlock): string | null {
|
|
|
34
33
|
|
|
35
34
|
function findFirstImagePath(items: ContentItem[]): string | null {
|
|
36
35
|
for (const item of items) {
|
|
37
|
-
if (isPageSection(item)) {
|
|
38
|
-
// Check inside PageSection blocks for image/cover blocks
|
|
39
|
-
const section = item as PageSection;
|
|
40
|
-
const sectionBlock = Array.isArray(section.block) ? section.block[0] : undefined;
|
|
41
|
-
if (sectionBlock) {
|
|
42
|
-
const path = getBlockImagePath(sectionBlock as unknown as ContentBlock);
|
|
43
|
-
if (path) return path;
|
|
44
|
-
}
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
36
|
if (isParallaxGroup(item)) {
|
|
48
37
|
// Check first slide's background image for preloading
|
|
49
38
|
const group = item as ParallaxGroup;
|
|
@@ -134,8 +123,6 @@ export default function PageRenderer({ page }: { page: Page }) {
|
|
|
134
123
|
? <ParallaxGroupRenderer key={item._key} group={item as ParallaxGroup} pageEnterAnimation={pageEnterAnimation} />
|
|
135
124
|
: isPageSectionV2(item)
|
|
136
125
|
? <SectionV2Renderer key={item._key} section={item as PageSectionV2} pageEnterAnimation={pageEnterAnimation} />
|
|
137
|
-
: isPageSection(item)
|
|
138
|
-
? <SectionRenderer key={item._key} section={item as PageSection} pageEnterAnimation={pageEnterAnimation} />
|
|
139
126
|
: null
|
|
140
127
|
)}
|
|
141
128
|
</article>
|
|
@@ -62,8 +62,6 @@ export default function ProjectGridBlockRenderer({
|
|
|
62
62
|
const viewport = useViewport();
|
|
63
63
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
64
64
|
const roRef = useRef<ResizeObserver | null>(null);
|
|
65
|
-
/** Container top offset at first measure — for above-the-fold detection */
|
|
66
|
-
const containerTopRef = useRef<number | null>(null);
|
|
67
65
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
68
66
|
const [resolvedProjects, setResolvedProjects] = useState<ResolvedProject[]>([]);
|
|
69
67
|
// BLK-005: Track fetch errors so we can provide user feedback
|
|
@@ -85,10 +83,6 @@ export default function ProjectGridBlockRenderer({
|
|
|
85
83
|
const w = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
86
84
|
if (w > 0) setContainerWidth(w);
|
|
87
85
|
}
|
|
88
|
-
// Capture container top on first observation (for above-the-fold detection)
|
|
89
|
-
if (containerTopRef.current === null) {
|
|
90
|
-
containerTopRef.current = node.getBoundingClientRect().top;
|
|
91
|
-
}
|
|
92
86
|
});
|
|
93
87
|
roRef.current = ro;
|
|
94
88
|
ro.observe(node);
|
|
@@ -227,39 +221,44 @@ export default function ProjectGridBlockRenderer({
|
|
|
227
221
|
}
|
|
228
222
|
: undefined;
|
|
229
223
|
|
|
230
|
-
// ───
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (item.y < cutoff) {
|
|
245
|
-
keys.add(item.key);
|
|
246
|
-
}
|
|
224
|
+
// ─── Grid-level entrance trigger ───
|
|
225
|
+
// A single IntersectionObserver on the grid container fires all card
|
|
226
|
+
// animations at once (with stagger). No per-card observer needed.
|
|
227
|
+
const [gridVisible, setGridVisible] = useState(false);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!entranceEnabled || gridVisible) return;
|
|
231
|
+
const el = containerRef.current;
|
|
232
|
+
if (!el) return;
|
|
233
|
+
|
|
234
|
+
// Respect prefers-reduced-motion
|
|
235
|
+
if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
236
|
+
setGridVisible(true);
|
|
237
|
+
return;
|
|
247
238
|
}
|
|
248
|
-
|
|
249
|
-
|
|
239
|
+
|
|
240
|
+
const observer = new IntersectionObserver(
|
|
241
|
+
([entry]) => {
|
|
242
|
+
if (entry.isIntersecting) {
|
|
243
|
+
setGridVisible(true);
|
|
244
|
+
observer.disconnect();
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{ threshold: 0.05 },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
observer.observe(el);
|
|
251
|
+
return () => observer.disconnect();
|
|
252
|
+
}, [entranceEnabled, gridVisible]);
|
|
250
253
|
|
|
251
254
|
// ─── Compute stagger indices sorted by vertical position ───
|
|
252
255
|
// Cards at similar y positions (within half of gapV) share the same rank.
|
|
253
|
-
//
|
|
256
|
+
// Computed for ALL cards so the entire grid animates in together.
|
|
254
257
|
const staggerIndices = useMemo(() => {
|
|
255
258
|
if (!entranceEnabled || masonry.items.length === 0) return new Map<string, number>();
|
|
256
259
|
|
|
257
|
-
// Only stagger above-the-fold cards
|
|
258
|
-
const aboveFold = masonry.items.filter((item) => aboveFoldKeys.has(item.key));
|
|
259
|
-
if (aboveFold.length === 0) return new Map<string, number>();
|
|
260
|
-
|
|
261
260
|
// Sort items by y, then by x for tie-breaking
|
|
262
|
-
const sorted = [...
|
|
261
|
+
const sorted = [...masonry.items].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
263
262
|
const indices = new Map<string, number>();
|
|
264
263
|
let rank = 0;
|
|
265
264
|
let prevY = -Infinity;
|
|
@@ -273,7 +272,7 @@ export default function ProjectGridBlockRenderer({
|
|
|
273
272
|
indices.set(item.key, rank);
|
|
274
273
|
}
|
|
275
274
|
return indices;
|
|
276
|
-
}, [entranceEnabled, masonry.items,
|
|
275
|
+
}, [entranceEnabled, masonry.items, gapV]);
|
|
277
276
|
|
|
278
277
|
if (resolvedProjects.length === 0) {
|
|
279
278
|
// BLK-005: Show subtle error message if fetch failed (not just empty data)
|
|
@@ -321,12 +320,10 @@ export default function ProjectGridBlockRenderer({
|
|
|
321
320
|
/>
|
|
322
321
|
);
|
|
323
322
|
|
|
324
|
-
const isAboveFold = aboveFoldKeys.has(item.key);
|
|
325
|
-
|
|
326
323
|
return (
|
|
327
324
|
<div
|
|
328
325
|
key={item.key}
|
|
329
|
-
data-card-entrance={entranceEnabled
|
|
326
|
+
data-card-entrance={entranceEnabled ? "" : undefined}
|
|
330
327
|
style={{
|
|
331
328
|
position: "absolute",
|
|
332
329
|
left: item.x,
|
|
@@ -335,11 +332,12 @@ export default function ProjectGridBlockRenderer({
|
|
|
335
332
|
height: item.height,
|
|
336
333
|
}}
|
|
337
334
|
>
|
|
338
|
-
{entranceEnabled && entranceAnimConfig
|
|
335
|
+
{entranceEnabled && entranceAnimConfig ? (
|
|
339
336
|
<EnterAnimationWrapper
|
|
340
337
|
config={entranceAnimConfig}
|
|
341
338
|
staggerIndex={staggerIndices.get(item.key) ?? 0}
|
|
342
339
|
staggerDelay={entranceStaggerDelay}
|
|
340
|
+
forceEnter={gridVisible}
|
|
343
341
|
style={{ width: "100%", height: "100%" }}
|
|
344
342
|
>
|
|
345
343
|
{card}
|
|
@@ -87,7 +87,7 @@ export default function TextBlockRenderer({ block }: { block: TextBlock }) {
|
|
|
87
87
|
return (
|
|
88
88
|
<div
|
|
89
89
|
className={`${className} space-y-[0.75em]`}
|
|
90
|
-
style={{ overflowWrap: "break-word", wordBreak: "
|
|
90
|
+
style={{ overflowWrap: "break-word", wordBreak: "normal", minWidth: 0, ...style }}
|
|
91
91
|
>
|
|
92
92
|
<PortableText value={block.text} />
|
|
93
93
|
</div>
|
|
@@ -100,22 +100,6 @@ const RowDragOverlay = memo(function RowDragOverlay({ rowKey }: { rowKey: string
|
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// PageSection
|
|
104
|
-
if ((item as { _type?: string })._type === "pageSection") {
|
|
105
|
-
const section = item as import("../../lib/sanity/types").PageSection;
|
|
106
|
-
const label = section.section_type === "projectGrid"
|
|
107
|
-
? "Project Grid"
|
|
108
|
-
: "Section";
|
|
109
|
-
return (
|
|
110
|
-
<div className="rounded border border-[#93278f] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#93278f]/20 backdrop-blur-sm">
|
|
111
|
-
<div className="flex items-center gap-2">
|
|
112
|
-
<span className="text-[#93278f]">⠿</span>
|
|
113
|
-
<span className="text-xs text-white">{label}</span>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
103
|
return null;
|
|
120
104
|
});
|
|
121
105
|
|
|
@@ -140,7 +124,7 @@ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: {
|
|
|
140
124
|
}
|
|
141
125
|
if (!item) return null;
|
|
142
126
|
|
|
143
|
-
// Handle V2 sections
|
|
127
|
+
// Handle V2 sections and virtual sections from parallax slides
|
|
144
128
|
let block: import("../../lib/sanity/types").ContentBlock | undefined;
|
|
145
129
|
if (isPageSectionV2(item)) {
|
|
146
130
|
const v2Section = item as PageSectionV2;
|
|
@@ -148,18 +132,12 @@ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: {
|
|
|
148
132
|
const found = (col.blocks || []).find((b) => b._key === blockKey);
|
|
149
133
|
if (found) { block = found; break; }
|
|
150
134
|
}
|
|
151
|
-
} else if ((item as { _type?: string })._type === "pageSection") {
|
|
152
|
-
const section = item as import("../../lib/sanity/types").PageSection;
|
|
153
|
-
const sBlock = Array.isArray(section.block) ? section.block[0] : undefined;
|
|
154
|
-
if (sBlock && sBlock._key === blockKey) {
|
|
155
|
-
block = sBlock as import("../../lib/sanity/types").ContentBlock;
|
|
156
|
-
}
|
|
157
135
|
}
|
|
158
136
|
|
|
159
137
|
if (!block) return null;
|
|
160
138
|
const info = ALL_BLOCK_INFO.find((b) => b.type === block!._type);
|
|
161
139
|
return (
|
|
162
|
-
<div className="rounded border border-[#
|
|
140
|
+
<div className="rounded border border-[#0d9668] bg-[#0d9668]/10 px-3 py-2 shadow-lg shadow-[#0d9668]/20 backdrop-blur-sm">
|
|
163
141
|
<div className="flex items-center gap-2">
|
|
164
142
|
<span className="text-xs">{info?.icon || "▪"}</span>
|
|
165
143
|
<span className="text-xs text-white">{info?.label || block._type}</span>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useMemo, memo } from "react";
|
|
4
4
|
import type { CascadeColumn } from "../../lib/builder/cascade-helpers";
|
|
5
5
|
import type { DropTarget } from "./hooks/useColumnDrag";
|
|
6
|
-
import {
|
|
6
|
+
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
7
7
|
|
|
8
8
|
// ============================================
|
|
9
9
|
// InsertionLines — Visual insertion indicators between adjacent columns
|
|
@@ -137,7 +137,7 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
137
137
|
pointerEvents: "auto",
|
|
138
138
|
}}
|
|
139
139
|
>
|
|
140
|
-
{/* Visual insertion line —
|
|
140
|
+
{/* Visual insertion line — blue when active */}
|
|
141
141
|
<div
|
|
142
142
|
style={{
|
|
143
143
|
position: "absolute",
|
|
@@ -146,7 +146,7 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
146
146
|
bottom: 4,
|
|
147
147
|
width: isActive ? 4 : 0,
|
|
148
148
|
transform: "translateX(-50%)",
|
|
149
|
-
background:
|
|
149
|
+
background: BUILDER_BLUE,
|
|
150
150
|
borderRadius: 2,
|
|
151
151
|
transition: "width 100ms ease-out, opacity 100ms ease-out",
|
|
152
152
|
opacity: isActive ? 1 : 0,
|
|
@@ -163,9 +163,9 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
163
163
|
width: 14,
|
|
164
164
|
height: 14,
|
|
165
165
|
borderRadius: "50%",
|
|
166
|
-
background:
|
|
166
|
+
background: BUILDER_BLUE,
|
|
167
167
|
border: "2px solid white",
|
|
168
|
-
boxShadow: "0 1px 4px rgba(
|
|
168
|
+
boxShadow: "0 1px 4px rgba(7, 107, 255, 0.4)",
|
|
169
169
|
display: "flex",
|
|
170
170
|
alignItems: "center",
|
|
171
171
|
justifyContent: "center",
|
|
@@ -18,16 +18,15 @@
|
|
|
18
18
|
|
|
19
19
|
import { memo, useMemo, useState, useEffect } from "react";
|
|
20
20
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
21
|
-
import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
|
|
22
21
|
import type { DeviceViewport } from "../../lib/builder/types";
|
|
23
|
-
import type { ContentItem,
|
|
24
|
-
import {
|
|
22
|
+
import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
|
|
23
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } from "../../lib/sanity/types";
|
|
25
24
|
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
26
|
-
import { getEffectiveColumnsV2, getSectionV2SettingValue
|
|
25
|
+
import { getEffectiveColumnsV2, getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
|
|
27
26
|
import BlockLivePreview from "./BlockLivePreview";
|
|
28
|
-
import {
|
|
27
|
+
import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
29
28
|
|
|
30
|
-
//
|
|
29
|
+
// Layout keys that support responsive overrides for V2 sections
|
|
31
30
|
const OVERRIDABLE_KEYS = [
|
|
32
31
|
"spacing_top", "spacing_right", "spacing_bottom", "spacing_left",
|
|
33
32
|
"offset_top", "offset_right", "offset_bottom", "offset_left",
|
|
@@ -35,43 +34,6 @@ const OVERRIDABLE_KEYS = [
|
|
|
35
34
|
"border_color", "border_width", "border_style", "border_sides", "border_radius",
|
|
36
35
|
] as const;
|
|
37
36
|
|
|
38
|
-
// ============================================
|
|
39
|
-
// Memoized per-section renderer — renders PageSection block directly
|
|
40
|
-
// ============================================
|
|
41
|
-
|
|
42
|
-
interface ReadOnlySectionProps {
|
|
43
|
-
section: PageSection;
|
|
44
|
-
viewport: DeviceViewport;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const ReadOnlySection = memo(function ReadOnlySection({ section, viewport }: ReadOnlySectionProps) {
|
|
48
|
-
const block = section.block?.[0];
|
|
49
|
-
if (!block) return null;
|
|
50
|
-
|
|
51
|
-
const base = section.settings || {};
|
|
52
|
-
// Merge responsive overrides for V1 PageSection (same approach as V2)
|
|
53
|
-
const resolvedSettings = useMemo(() => {
|
|
54
|
-
const merged: Record<string, unknown> = { ...base };
|
|
55
|
-
for (const key of OVERRIDABLE_KEYS) {
|
|
56
|
-
merged[key] = getRowSettingValue(section, viewport, key, (base as Record<string, unknown>)[key]);
|
|
57
|
-
}
|
|
58
|
-
return merged;
|
|
59
|
-
}, [section, viewport, base]);
|
|
60
|
-
const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
|
|
61
|
-
|
|
62
|
-
const sectionStyle: React.CSSProperties = {
|
|
63
|
-
...layoutStyles,
|
|
64
|
-
contentVisibility: "auto",
|
|
65
|
-
containIntrinsicSize: "auto 200px",
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div style={sectionStyle}>
|
|
70
|
-
<BlockLivePreview block={block} viewport={viewport} />
|
|
71
|
-
</div>
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
37
|
// ============================================
|
|
76
38
|
// Memoized per-section V2 renderer — renders PageSectionV2 grid
|
|
77
39
|
// BUG-V2-005 fix: V2 sections were not rendered in inactive device frames.
|
|
@@ -404,12 +366,6 @@ export default function ReadOnlyFrame({ viewport }: ReadOnlyFrameProps) {
|
|
|
404
366
|
section={item as PageSectionV2}
|
|
405
367
|
viewport={viewport}
|
|
406
368
|
/>
|
|
407
|
-
) : isPageSection(item) ? (
|
|
408
|
-
<ReadOnlySection
|
|
409
|
-
key={item._key}
|
|
410
|
-
section={item as PageSection}
|
|
411
|
-
viewport={viewport}
|
|
412
|
-
/>
|
|
413
369
|
) : null
|
|
414
370
|
)}
|
|
415
371
|
</div>
|
|
@@ -277,9 +277,9 @@ export default function SectionV2Canvas({
|
|
|
277
277
|
}}
|
|
278
278
|
className={`rounded-lg border-2 border-dashed text-xs font-medium transition-all flex items-center justify-center cursor-pointer ${
|
|
279
279
|
isGapTarget
|
|
280
|
-
? "border-
|
|
280
|
+
? "border-blue-500 text-blue-500 bg-blue-500/10 opacity-100"
|
|
281
281
|
: showAsDropTarget
|
|
282
|
-
? "border-
|
|
282
|
+
? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
|
|
283
283
|
: isSectionHovered
|
|
284
284
|
? "border-[#076bff]/25 text-[#076bff]/50 hover:text-[#076bff] hover:border-[#076bff]/60 hover:bg-[#076bff]/5 opacity-100"
|
|
285
285
|
: "border-transparent text-transparent opacity-0 pointer-events-none"
|
|
@@ -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 { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
13
|
-
import { BUILDER_BLUE
|
|
13
|
+
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
14
14
|
|
|
15
15
|
// ============================================
|
|
16
16
|
// SectionV2Column — Individual column in a V2 section grid
|
|
@@ -266,7 +266,7 @@ export default function SectionV2Column({
|
|
|
266
266
|
style={{
|
|
267
267
|
transition: "box-shadow 150ms, border 150ms",
|
|
268
268
|
...(isSwapTarget
|
|
269
|
-
? { boxShadow: `inset 0 0 0 2px ${
|
|
269
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(7, 107, 255, 0.08)" }
|
|
270
270
|
: isBlockOver
|
|
271
271
|
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
|
|
272
272
|
: isSelected
|
|
@@ -450,9 +450,9 @@ export default function SectionV2Column({
|
|
|
450
450
|
aria-label="Add block to empty column"
|
|
451
451
|
className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
|
|
452
452
|
showChrome
|
|
453
|
-
? "bg-[#
|
|
453
|
+
? "bg-[#0d9668] text-white hover:bg-[#0a7d56] shadow-sm opacity-100"
|
|
454
454
|
: showFaintOutline
|
|
455
|
-
? "bg-[#
|
|
455
|
+
? "bg-[#0d9668]/30 text-white/50 opacity-40"
|
|
456
456
|
: "bg-transparent text-transparent opacity-0 pointer-events-none"
|
|
457
457
|
}`}
|
|
458
458
|
style={{ pointerEvents: showChrome || showFaintOutline ? "auto" : "none" }}
|
|
@@ -476,7 +476,7 @@ export default function SectionV2Column({
|
|
|
476
476
|
<button
|
|
477
477
|
onClick={handleAddBlockBelow}
|
|
478
478
|
aria-label="Add block below existing blocks"
|
|
479
|
-
className="w-full py-1.5 text-[11px] font-medium rounded bg-[#
|
|
479
|
+
className="w-full py-1.5 text-[11px] font-medium rounded bg-[#0d9668] text-white hover:bg-[#0a7d56] transition-all shadow-sm"
|
|
480
480
|
style={{ pointerEvents: showChrome ? "auto" : "none" }}
|
|
481
481
|
>
|
|
482
482
|
+ Add Block
|
|
@@ -27,7 +27,6 @@ import { AnimationTab } from "./settings-panel/AnimationTab";
|
|
|
27
27
|
import { ColumnV2AnimationTab } from "./settings-panel/ColumnV2AnimationTab";
|
|
28
28
|
import { CustomSectionSettings } from "./settings-panel/CustomSectionSettings";
|
|
29
29
|
import {
|
|
30
|
-
LayoutTab,
|
|
31
30
|
BlockLayoutTab,
|
|
32
31
|
PageSettings,
|
|
33
32
|
PageSeoSettings,
|
|
@@ -48,7 +47,6 @@ export default function SettingsPanel() {
|
|
|
48
47
|
|
|
49
48
|
const sel = useSettingsPanelSelection();
|
|
50
49
|
const {
|
|
51
|
-
selectedSection,
|
|
52
50
|
selectedSectionV2,
|
|
53
51
|
selectedCustomSectionInstance,
|
|
54
52
|
selectedParallaxGroup,
|
|
@@ -354,14 +352,9 @@ export default function SettingsPanel() {
|
|
|
354
352
|
) : activeTab === "animation" ? (
|
|
355
353
|
<AnimationTab
|
|
356
354
|
selectedBlock={selectedBlock}
|
|
357
|
-
selectedSection={selectedSection}
|
|
358
355
|
/>
|
|
359
356
|
) : activeTab === "layout" ? (
|
|
360
357
|
(() => {
|
|
361
|
-
// PageSection: show section layout settings (spacing, background, border)
|
|
362
|
-
if (selectedSection) {
|
|
363
|
-
return <LayoutTab section={selectedSection} sectionKey={selectedSection._key} />;
|
|
364
|
-
}
|
|
365
358
|
if (selectedBlock && !selectedBlock.isSection) {
|
|
366
359
|
return <BlockLayoutTab block={selectedBlock.block} />;
|
|
367
360
|
}
|
|
@@ -377,11 +370,6 @@ export default function SettingsPanel() {
|
|
|
377
370
|
})()
|
|
378
371
|
) : activeTab === "seo" ? (
|
|
379
372
|
<PageSeoSettings />
|
|
380
|
-
) : selectedSection ? (
|
|
381
|
-
// PageSection selected → show the section block's settings directly
|
|
382
|
-
<BlockSettings
|
|
383
|
-
block={selectedSection.block[0]}
|
|
384
|
-
/>
|
|
385
373
|
) : selectedBlock ? (
|
|
386
374
|
<BlockSettings
|
|
387
375
|
block={selectedBlock.block}
|
|
@@ -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) {
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
4
|
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
5
5
|
import type { ButtonBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
6
|
+
import {
|
|
7
|
+
ContentIcon,
|
|
8
|
+
StyleIcon,
|
|
9
|
+
OptionsIcon,
|
|
10
|
+
} from "./section-icons";
|
|
6
11
|
import {
|
|
7
12
|
SettingsField,
|
|
8
13
|
SettingsSection,
|
|
@@ -61,7 +66,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
61
66
|
<>
|
|
62
67
|
<ViewportBadge />
|
|
63
68
|
|
|
64
|
-
<SettingsSection title="Content" defaultOpen>
|
|
69
|
+
<SettingsSection title="Content" defaultOpen icon={<ContentIcon />}>
|
|
65
70
|
<SettingsField label="Button Text">
|
|
66
71
|
<input
|
|
67
72
|
type="text"
|
|
@@ -84,7 +89,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
84
89
|
</SettingsField>
|
|
85
90
|
</SettingsSection>
|
|
86
91
|
|
|
87
|
-
<SettingsSection title="Style">
|
|
92
|
+
<SettingsSection title="Style" icon={<StyleIcon />}>
|
|
88
93
|
<SettingsField label="Variant">
|
|
89
94
|
<div className="flex gap-1">
|
|
90
95
|
{(["primary", "secondary", "outline", "text"] as const).map(
|
|
@@ -152,7 +157,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
152
157
|
</ResponsiveField>
|
|
153
158
|
</SettingsSection>
|
|
154
159
|
|
|
155
|
-
<SettingsSection title="Options">
|
|
160
|
+
<SettingsSection title="Options" icon={<OptionsIcon />}>
|
|
156
161
|
<StyledCheckbox
|
|
157
162
|
label="Open in new tab"
|
|
158
163
|
checked={block.target || false}
|