@morphika/andami 0.2.26 → 0.4.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 +41 -47
- package/app/api/admin/assets/scan/route.ts +40 -13
- package/app/api/admin/custom-sections/[slug]/route.ts +4 -1
- package/app/api/admin/custom-sections/route.ts +4 -1
- package/app/api/admin/pages/[slug]/route.ts +7 -1
- package/app/api/admin/pages/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +19 -1
- package/app/api/admin/r2/disconnect/route.ts +3 -0
- package/app/api/admin/r2/rename/route.ts +52 -13
- package/app/api/admin/r2/upload-url/route.ts +8 -1
- package/app/api/admin/settings/route.ts +4 -1
- package/app/api/admin/styles/route.ts +4 -1
- package/components/admin/styles/GridLayoutEditor.tsx +46 -46
- package/components/blocks/BlockRenderer.tsx +15 -2
- package/components/blocks/CoverSectionRenderer.tsx +75 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +17 -11
- package/components/blocks/ParallaxGroupRenderer.tsx +45 -10
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
- package/components/blocks/ShaderCanvas.tsx +10 -6
- package/components/builder/BlockCardIcons.tsx +227 -0
- package/components/builder/BlockLivePreview.tsx +5 -0
- package/components/builder/BlockTypePicker.tsx +36 -63
- package/components/builder/BuilderCanvas.tsx +6 -2
- package/components/builder/ColumnDragOverlay.tsx +3 -3
- package/components/builder/CoverRowResizeHandle.tsx +5 -2
- package/components/builder/CoverSectionCanvas.tsx +45 -52
- package/components/builder/DndWrapper.tsx +1 -1
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/ParallaxGroupCanvas.tsx +12 -71
- package/components/builder/ReadOnlyFrame.tsx +4 -23
- package/components/builder/SectionCardIcons.tsx +320 -0
- package/components/builder/SectionEditorBar.tsx +17 -12
- package/components/builder/SectionTypePicker.tsx +34 -138
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +19 -30
- package/components/builder/SettingsPanel.tsx +8 -32
- package/components/builder/SortableBlock.tsx +42 -50
- package/components/builder/SortableRow.tsx +207 -19
- package/components/builder/blockStyles.tsx +59 -180
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
- package/components/builder/editors/index.ts +1 -0
- package/components/builder/iconPrimitives.tsx +78 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
- package/components/builder/live-preview/index.ts +1 -0
- package/components/builder/settings-panel/BlockSettings.tsx +7 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/assets.ts +17 -2
- package/lib/builder/block-registrations.ts +268 -0
- package/lib/builder/block-registry.ts +195 -0
- package/lib/builder/constants.ts +22 -15
- package/lib/builder/defaults.ts +21 -0
- package/lib/builder/format.ts +25 -0
- package/lib/builder/history.ts +0 -3
- package/lib/builder/index.ts +16 -0
- package/lib/builder/layout-styles.ts +1 -1
- package/lib/builder/registry.ts +44 -0
- package/lib/builder/section-visibility.ts +36 -0
- package/lib/builder/serializer/normalizers.ts +15 -6
- package/lib/builder/serializer/serializers.ts +3 -3
- package/lib/builder/store-blocks.ts +16 -9
- package/lib/builder/store-cover.ts +76 -8
- package/lib/builder/store-sections.ts +1 -1
- package/lib/builder/store.ts +0 -2
- package/lib/builder/types.ts +9 -5
- package/lib/csrf.ts +31 -0
- package/lib/sanity/types.ts +54 -2
- package/lib/security.ts +50 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/index.ts +2 -1
- package/sanity/schemas/blocks/projectCarouselBlock.ts +218 -0
- package/sanity/schemas/index.ts +4 -1
- package/sanity/schemas/objects/coverSection.ts +35 -3
- package/sanity/schemas/pageSectionV2.ts +1 -0
- package/components/builder/ParallaxSlideHeader.tsx +0 -113
|
@@ -15,74 +15,74 @@ const DEFAULT_GRID = {
|
|
|
15
15
|
|
|
16
16
|
/** SVG illustration for the Column Gutter card */
|
|
17
17
|
function GutterIcon() {
|
|
18
|
+
const ACCENT = "#4794E2";
|
|
18
19
|
return (
|
|
19
|
-
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full"
|
|
20
|
-
{/* 5 columns */}
|
|
21
|
-
<rect x="6" y="8" width="16" height="56" rx="2" fill="
|
|
22
|
-
<rect x="28" y="8" width="16" height="56" rx="2" fill="
|
|
23
|
-
<rect x="50" y="8" width="16" height="56" rx="2" fill="
|
|
24
|
-
<rect x="72" y="8" width="16" height="56" rx="2" fill="
|
|
25
|
-
<rect x="94" y="8" width="16" height="56" rx="2" fill="
|
|
26
|
-
{/* Gap arrows between columns */}
|
|
27
|
-
<line x1="22" y1="36" x2="28" y2="36" stroke=
|
|
28
|
-
<path d="M26 34l2 2-2 2" stroke=
|
|
29
|
-
<path d="M24 34l-2 2 2 2" stroke=
|
|
30
|
-
<line x1="44" y1="36" x2="50" y2="36" stroke=
|
|
31
|
-
<path d="M48 34l2 2-2 2" stroke=
|
|
32
|
-
<path d="M46 34l-2 2 2 2" stroke=
|
|
33
|
-
<line x1="66" y1="36" x2="72" y2="36" stroke=
|
|
34
|
-
<path d="M70 34l2 2-2 2" stroke=
|
|
35
|
-
<path d="M68 34l-2 2 2 2" stroke=
|
|
36
|
-
<line x1="88" y1="36" x2="94" y2="36" stroke=
|
|
37
|
-
<path d="M92 34l2 2-2 2" stroke=
|
|
38
|
-
<path d="M90 34l-2 2 2 2" stroke=
|
|
20
|
+
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full">
|
|
21
|
+
{/* 5 columns (ghost blue) */}
|
|
22
|
+
<rect x="6" y="8" width="16" height="56" rx="2" fill="#DDE6F5" />
|
|
23
|
+
<rect x="28" y="8" width="16" height="56" rx="2" fill="#DDE6F5" />
|
|
24
|
+
<rect x="50" y="8" width="16" height="56" rx="2" fill="#DDE6F5" />
|
|
25
|
+
<rect x="72" y="8" width="16" height="56" rx="2" fill="#DDE6F5" />
|
|
26
|
+
<rect x="94" y="8" width="16" height="56" rx="2" fill="#DDE6F5" />
|
|
27
|
+
{/* Gap arrows between columns (accent) */}
|
|
28
|
+
<line x1="22" y1="36" x2="28" y2="36" stroke={ACCENT} />
|
|
29
|
+
<path d="M26 34l2 2-2 2" stroke={ACCENT} fill="none" />
|
|
30
|
+
<path d="M24 34l-2 2 2 2" stroke={ACCENT} fill="none" />
|
|
31
|
+
<line x1="44" y1="36" x2="50" y2="36" stroke={ACCENT} />
|
|
32
|
+
<path d="M48 34l2 2-2 2" stroke={ACCENT} fill="none" />
|
|
33
|
+
<path d="M46 34l-2 2 2 2" stroke={ACCENT} fill="none" />
|
|
34
|
+
<line x1="66" y1="36" x2="72" y2="36" stroke={ACCENT} />
|
|
35
|
+
<path d="M70 34l2 2-2 2" stroke={ACCENT} fill="none" />
|
|
36
|
+
<path d="M68 34l-2 2 2 2" stroke={ACCENT} fill="none" />
|
|
37
|
+
<line x1="88" y1="36" x2="94" y2="36" stroke={ACCENT} />
|
|
38
|
+
<path d="M92 34l2 2-2 2" stroke={ACCENT} fill="none" />
|
|
39
|
+
<path d="M90 34l-2 2 2 2" stroke={ACCENT} fill="none" />
|
|
39
40
|
</svg>
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/** SVG illustration for the Max Page Width card */
|
|
44
45
|
function MaxWidthIcon() {
|
|
46
|
+
// Frame/dots/content stay neutral; arrows + dashed boundaries are the accent.
|
|
47
|
+
const ACCENT = "#4794E2";
|
|
45
48
|
return (
|
|
46
|
-
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full"
|
|
49
|
+
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full">
|
|
47
50
|
{/* Browser frame */}
|
|
48
|
-
<rect x="10" y="10" width="100" height="52" rx="4" stroke="
|
|
51
|
+
<rect x="10" y="10" width="100" height="52" rx="4" stroke="#b0b5bd" strokeWidth="1.5" opacity="0.3" />
|
|
49
52
|
{/* Title bar dots */}
|
|
50
|
-
<circle cx="18" cy="17" r="1.5" fill="
|
|
51
|
-
<circle cx="23" cy="17" r="1.5" fill="
|
|
52
|
-
<circle cx="28" cy="17" r="1.5" fill="
|
|
53
|
+
<circle cx="18" cy="17" r="1.5" fill="#b0b5bd" opacity="0.25" />
|
|
54
|
+
<circle cx="23" cy="17" r="1.5" fill="#b0b5bd" opacity="0.25" />
|
|
55
|
+
<circle cx="28" cy="17" r="1.5" fill="#b0b5bd" opacity="0.25" />
|
|
53
56
|
{/* Divider */}
|
|
54
|
-
<line x1="10" y1="22" x2="110" y2="22" stroke="
|
|
55
|
-
{/* Content area */}
|
|
56
|
-
<rect x="28" y="28" width="64" height="28" rx="2" fill="
|
|
57
|
+
<line x1="10" y1="22" x2="110" y2="22" stroke="#b0b5bd" strokeWidth="1" opacity="0.15" />
|
|
58
|
+
{/* Content area (ghost blue) */}
|
|
59
|
+
<rect x="28" y="28" width="64" height="28" rx="2" fill="#DDE6F5" />
|
|
57
60
|
{/* Left arrow */}
|
|
58
|
-
<line x1="
|
|
59
|
-
<path d="
|
|
61
|
+
<line x1="13" y1="42" x2="26" y2="42" stroke={ACCENT} strokeWidth="1.2" />
|
|
62
|
+
<path d="M24 40l2 2-2 2" stroke={ACCENT} strokeWidth="1.2" fill="none" />
|
|
60
63
|
{/* Right arrow */}
|
|
61
|
-
<line x1="
|
|
62
|
-
<path d="
|
|
64
|
+
<line x1="94" y1="42" x2="107" y2="42" stroke={ACCENT} strokeWidth="1.2" />
|
|
65
|
+
<path d="M96 40l-2 2 2 2" stroke={ACCENT} strokeWidth="1.2" fill="none" />
|
|
63
66
|
{/* Dashed boundary lines */}
|
|
64
|
-
<line x1="28" y1="26" x2="28" y2="58" stroke=
|
|
65
|
-
<line x1="92" y1="26" x2="92" y2="58" stroke=
|
|
67
|
+
<line x1="28" y1="26" x2="28" y2="58" stroke={ACCENT} strokeWidth="1" strokeDasharray="2 2" />
|
|
68
|
+
<line x1="92" y1="26" x2="92" y2="58" stroke={ACCENT} strokeWidth="1" strokeDasharray="2 2" />
|
|
66
69
|
</svg>
|
|
67
70
|
);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/** SVG illustration for the Scroll Animations card */
|
|
71
74
|
function ScrollAnimIcon() {
|
|
75
|
+
// 3 blue layers with increasing opacity convey the fade-in effect; accent arrow below.
|
|
76
|
+
const ACCENT = "#4794E2";
|
|
72
77
|
return (
|
|
73
|
-
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full"
|
|
74
|
-
{/* 3 stacked layers
|
|
75
|
-
<rect x="30" y="6" width="60" height="14" rx="3" fill=
|
|
76
|
-
<rect x="30" y="24" width="60" height="14" rx="3" fill=
|
|
77
|
-
<rect x="30" y="42" width="60" height="14" rx="3" fill=
|
|
78
|
+
<svg viewBox="0 0 120 72" fill="none" className="w-full h-full">
|
|
79
|
+
{/* 3 stacked layers (fade-in effect) */}
|
|
80
|
+
<rect x="30" y="6" width="60" height="14" rx="3" fill={ACCENT} opacity="0.06" />
|
|
81
|
+
<rect x="30" y="24" width="60" height="14" rx="3" fill={ACCENT} opacity="0.13" />
|
|
82
|
+
<rect x="30" y="42" width="60" height="14" rx="3" fill={ACCENT} opacity="0.36" />
|
|
78
83
|
{/* Scroll-down arrow */}
|
|
79
|
-
<line x1="60" y1="60" x2="60" y2="70" stroke=
|
|
80
|
-
<path d="M56 67l4 4 4-4" stroke=
|
|
81
|
-
{/* Motion lines */}
|
|
82
|
-
<line x1="24" y1="46" x2="28" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
|
|
83
|
-
<line x1="24" y1="52" x2="28" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
|
|
84
|
-
<line x1="92" y1="46" x2="96" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
|
|
85
|
-
<line x1="92" y1="52" x2="96" y2="49" stroke="currentColor" strokeWidth="1" opacity="0.3" />
|
|
84
|
+
<line x1="60" y1="60" x2="60" y2="70" stroke={ACCENT} strokeWidth="1.5" />
|
|
85
|
+
<path d="M56 67l4 4 4-4" stroke={ACCENT} strokeWidth="1.5" fill="none" />
|
|
86
86
|
</svg>
|
|
87
87
|
);
|
|
88
88
|
}
|
|
@@ -24,6 +24,7 @@ import VideoBlockRenderer from "./VideoBlockRenderer";
|
|
|
24
24
|
import SpacerBlockRenderer from "./SpacerBlockRenderer";
|
|
25
25
|
import ButtonBlockRenderer from "./ButtonBlockRenderer";
|
|
26
26
|
import ProjectGridBlockRenderer from "./ProjectGridBlockRenderer";
|
|
27
|
+
import ProjectCarouselBlockRenderer from "./ProjectCarouselBlockRenderer";
|
|
27
28
|
|
|
28
29
|
// ── BLK-003: Error Boundary for block renderers ──
|
|
29
30
|
// Prevents a single broken block from crashing the entire page.
|
|
@@ -310,6 +311,9 @@ export default function BlockRenderer({
|
|
|
310
311
|
case "projectGridBlock":
|
|
311
312
|
content = <ProjectGridBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectGridBlock} />;
|
|
312
313
|
break;
|
|
314
|
+
case "projectCarouselBlock":
|
|
315
|
+
content = <ProjectCarouselBlockRenderer block={resolved as import("../../lib/sanity/types").ProjectCarouselBlock} />;
|
|
316
|
+
break;
|
|
313
317
|
default: {
|
|
314
318
|
const unknownBlock = resolved as ContentBlock;
|
|
315
319
|
if (process.env.NODE_ENV === "development") {
|
|
@@ -418,13 +422,22 @@ export default function BlockRenderer({
|
|
|
418
422
|
const isShader = isShaderPreset(blockHoverEffect.preset);
|
|
419
423
|
if (isShader) {
|
|
420
424
|
if (resolved._type === "imageBlock") {
|
|
421
|
-
|
|
425
|
+
const imgPath = (resolved as import("../../lib/sanity/types").ImageBlock).asset_path;
|
|
426
|
+
if (imgPath) {
|
|
427
|
+
shaderSrc = resolveAsset(imgPath);
|
|
428
|
+
}
|
|
422
429
|
const br = (resolved as import("../../lib/sanity/types").ImageBlock).border_radius;
|
|
423
430
|
if (br) shaderBorderRadius = `${String(br).replace(/px$/i, "")}px`;
|
|
424
431
|
}
|
|
425
432
|
// Shader preset without image src: skip wrapper entirely
|
|
426
433
|
if (!shaderSrc) {
|
|
427
|
-
//
|
|
434
|
+
// Warn in development so the missing asset is debuggable — silent in prod
|
|
435
|
+
if (process.env.NODE_ENV === "development") {
|
|
436
|
+
// eslint-disable-next-line no-console
|
|
437
|
+
console.warn(
|
|
438
|
+
`[BlockRenderer] Shader hover preset "${blockHoverEffect.preset}" on block ${block._key} (${resolved._type}) has no asset_path — falling back to no hover effect.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
428
441
|
} else {
|
|
429
442
|
content = (
|
|
430
443
|
<HoverAnimationWrapper
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Session 176: Cover Sections — Phase 9 (Public Renderer).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { useEffect, useRef } from "react";
|
|
13
14
|
import type {
|
|
14
15
|
CoverSection,
|
|
15
16
|
SectionColumn,
|
|
@@ -23,6 +24,13 @@ import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
|
23
24
|
import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
24
25
|
import { assetUrl } from "../../lib/assets";
|
|
25
26
|
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
27
|
+
import { normalizeRowHeights } from "../../lib/builder/store-cover";
|
|
28
|
+
import { useNavColor } from "../../lib/contexts/NavColorContext";
|
|
29
|
+
import { isValidHex } from "../../lib/color-utils";
|
|
30
|
+
import {
|
|
31
|
+
sectionVisibilityRatio,
|
|
32
|
+
NAV_COLOR_OVERRIDE_THRESHOLD,
|
|
33
|
+
} from "../../lib/builder/section-visibility";
|
|
26
34
|
|
|
27
35
|
interface CoverSectionRendererProps {
|
|
28
36
|
section: CoverSection;
|
|
@@ -51,10 +59,19 @@ function buildCoverResponsiveCss(section: CoverSection): string | null {
|
|
|
51
59
|
|
|
52
60
|
if (override.cover_rows && override.cover_rows.length > 0) {
|
|
53
61
|
const baseRows = section.cover_rows;
|
|
54
|
-
|
|
62
|
+
// Partial overrides mix desktop values with override values — the
|
|
63
|
+
// merged sum is not guaranteed to be 100. Normalize so the CSS
|
|
64
|
+
// grid-template-rows at that viewport stays valid. (Cover Sections
|
|
65
|
+
// audit bug #3.)
|
|
66
|
+
const mergedPercents = baseRows.map((r) => {
|
|
55
67
|
const rowOverride = override.cover_rows?.find((o) => o._key === r._key);
|
|
56
|
-
return
|
|
57
|
-
})
|
|
68
|
+
return rowOverride?.height_percent ?? r.height_percent;
|
|
69
|
+
});
|
|
70
|
+
const total = mergedPercents.reduce((a, b) => a + b, 0);
|
|
71
|
+
const finalPercents = Math.abs(total - 100) <= 0.5
|
|
72
|
+
? mergedPercents
|
|
73
|
+
: normalizeRowHeights(mergedPercents);
|
|
74
|
+
const rowTemplate = finalPercents.map((p) => `${p}%`).join(" ");
|
|
58
75
|
rules.push(`grid-template-rows:${rowTemplate}!important`);
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -87,6 +104,60 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
87
104
|
const colGap = s.col_gap ?? 20;
|
|
88
105
|
const rowGap = s.row_gap ?? 20;
|
|
89
106
|
|
|
107
|
+
// ── Navbar color override ────────────────────────────────────────────
|
|
108
|
+
// While this cover section is significantly on-screen (>30% of the
|
|
109
|
+
// viewport), push `nav_color` up through NavColorContext so the top
|
|
110
|
+
// nav text adopts it. When the next section takes over (or the cover
|
|
111
|
+
// scrolls off screen), clear the override so the page default returns.
|
|
112
|
+
const sectionRef = useRef<HTMLElement | null>(null);
|
|
113
|
+
const { setNavColor } = useNavColor();
|
|
114
|
+
const navColor = section.nav_color || "";
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const hasValidNavColor = navColor && isValidHex(navColor);
|
|
118
|
+
if (!hasValidNavColor) return;
|
|
119
|
+
|
|
120
|
+
// rAF-throttled scroll listener — cheap: one getBoundingClientRect per tick.
|
|
121
|
+
let ticking = false;
|
|
122
|
+
let rafId: number | null = null;
|
|
123
|
+
let overrideActive = false;
|
|
124
|
+
|
|
125
|
+
const update = () => {
|
|
126
|
+
ticking = false;
|
|
127
|
+
const el = sectionRef.current;
|
|
128
|
+
if (!el) return;
|
|
129
|
+
const rect = el.getBoundingClientRect();
|
|
130
|
+
const ratio = sectionVisibilityRatio(rect.top, rect.bottom, window.innerHeight);
|
|
131
|
+
if (ratio > NAV_COLOR_OVERRIDE_THRESHOLD) {
|
|
132
|
+
if (!overrideActive) overrideActive = true;
|
|
133
|
+
setNavColor(navColor);
|
|
134
|
+
} else if (overrideActive) {
|
|
135
|
+
overrideActive = false;
|
|
136
|
+
setNavColor("");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const onScroll = () => {
|
|
141
|
+
if (ticking) return;
|
|
142
|
+
ticking = true;
|
|
143
|
+
rafId = requestAnimationFrame(update);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Initial evaluation (e.g. cover is already the first section on mount)
|
|
147
|
+
update();
|
|
148
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
149
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
window.removeEventListener("scroll", onScroll);
|
|
153
|
+
window.removeEventListener("resize", onScroll);
|
|
154
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
155
|
+
// Release any override we set so the next section's nav color (or the
|
|
156
|
+
// page default) takes over cleanly.
|
|
157
|
+
if (overrideActive) setNavColor("");
|
|
158
|
+
};
|
|
159
|
+
}, [navColor, setNavColor]);
|
|
160
|
+
|
|
90
161
|
const rowTemplate = section.cover_rows
|
|
91
162
|
.map((r) => `${r.height_percent}%`)
|
|
92
163
|
.join(" ");
|
|
@@ -132,6 +203,7 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
132
203
|
|
|
133
204
|
const sectionContent = (
|
|
134
205
|
<section
|
|
206
|
+
ref={sectionRef}
|
|
135
207
|
style={{
|
|
136
208
|
position: "relative",
|
|
137
209
|
height: section.height,
|
|
@@ -455,6 +455,20 @@ export default function ImageGridBlockRenderer({
|
|
|
455
455
|
// Unique ID for phone responsive override — must be before early return (hooks rule)
|
|
456
456
|
const gridId = useRef(`ig-${Math.random().toString(36).slice(2, 8)}`).current;
|
|
457
457
|
|
|
458
|
+
// Stable lightbox callbacks — inline versions rebuild every render and
|
|
459
|
+
// caused the lightbox keydown listener to add/remove on every parent render.
|
|
460
|
+
// Functional setState keeps them closure-safe regardless of prop changes.
|
|
461
|
+
const imagesLen = block.images?.length ?? 0;
|
|
462
|
+
const handleLightboxClose = useCallback(() => setLightboxIndex(null), []);
|
|
463
|
+
const handleLightboxPrev = useCallback(() => {
|
|
464
|
+
if (imagesLen <= 0) return;
|
|
465
|
+
setLightboxIndex((prev) => (prev !== null ? (prev - 1 + imagesLen) % imagesLen : 0));
|
|
466
|
+
}, [imagesLen]);
|
|
467
|
+
const handleLightboxNext = useCallback(() => {
|
|
468
|
+
if (imagesLen <= 0) return;
|
|
469
|
+
setLightboxIndex((prev) => (prev !== null ? (prev + 1) % imagesLen : 0));
|
|
470
|
+
}, [imagesLen]);
|
|
471
|
+
|
|
458
472
|
// Block is already resolved for the current viewport by BlockRenderer
|
|
459
473
|
if (!block.images?.length) return null;
|
|
460
474
|
|
|
@@ -534,17 +548,9 @@ export default function ImageGridBlockRenderer({
|
|
|
534
548
|
images={images}
|
|
535
549
|
currentIndex={lightboxIndex}
|
|
536
550
|
resolveAsset={resolveAsset}
|
|
537
|
-
onClose={
|
|
538
|
-
onPrev={
|
|
539
|
-
|
|
540
|
-
prev !== null ? (prev - 1 + images.length) % images.length : 0
|
|
541
|
-
)
|
|
542
|
-
}
|
|
543
|
-
onNext={() =>
|
|
544
|
-
setLightboxIndex((prev) =>
|
|
545
|
-
prev !== null ? (prev + 1) % images.length : 0
|
|
546
|
-
)
|
|
547
|
-
}
|
|
551
|
+
onClose={handleLightboxClose}
|
|
552
|
+
onPrev={handleLightboxPrev}
|
|
553
|
+
onNext={handleLightboxNext}
|
|
548
554
|
/>
|
|
549
555
|
)}
|
|
550
556
|
</>
|
|
@@ -109,6 +109,45 @@ export default function ParallaxGroupRenderer({
|
|
|
109
109
|
|
|
110
110
|
// ── Nav color interpolation helper ──
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Cache the per-slide {center, color} array. Building it reads offsetTop /
|
|
114
|
+
* offsetHeight for every slide — a layout read that's cheap on its own but
|
|
115
|
+
* was previously done on every rAF tick inside the scroll loop, adding up
|
|
116
|
+
* to measurable jank on groups with 10+ slides + nav color interpolation.
|
|
117
|
+
* The cache is invalidated on resize and when `group.slides` changes.
|
|
118
|
+
*/
|
|
119
|
+
const slideDataCacheRef = useRef<{ center: number; color: string }[] | null>(null);
|
|
120
|
+
const slideDataDirtyRef = useRef(true);
|
|
121
|
+
|
|
122
|
+
const buildSlideData = useCallback((): { center: number; color: string }[] => {
|
|
123
|
+
const slideData: { center: number; color: string }[] = [];
|
|
124
|
+
const orderedSlides = group.slides || [];
|
|
125
|
+
for (let i = 0; i < orderedSlides.length; i++) {
|
|
126
|
+
const refs = slideRefsMap.current.get(orderedSlides[i]._key);
|
|
127
|
+
if (!refs?.slide) continue;
|
|
128
|
+
const el = refs.slide;
|
|
129
|
+
slideData.push({
|
|
130
|
+
center: el.offsetTop + el.offsetHeight / 2,
|
|
131
|
+
color: slideNavColors[i] || "",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return slideData;
|
|
135
|
+
}, [group.slides, slideNavColors]);
|
|
136
|
+
|
|
137
|
+
// Mark cache dirty when slides array or colors change
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
slideDataDirtyRef.current = true;
|
|
140
|
+
}, [group.slides, slideNavColors]);
|
|
141
|
+
|
|
142
|
+
// Invalidate cache on resize (layout shifts)
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const onResize = () => {
|
|
145
|
+
slideDataDirtyRef.current = true;
|
|
146
|
+
};
|
|
147
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
148
|
+
return () => window.removeEventListener("resize", onResize);
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
112
151
|
/**
|
|
113
152
|
* Compute the interpolated navbar color based on current scroll position.
|
|
114
153
|
* Uses slide centers: the viewport center's position between two adjacent
|
|
@@ -124,16 +163,12 @@ export default function ParallaxGroupRenderer({
|
|
|
124
163
|
|
|
125
164
|
const viewCenter = scrollY + vh / 2;
|
|
126
165
|
|
|
127
|
-
// Build ordered array of {center, color} for each slide
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const refs = slideRefsMap.current.get(orderedSlides[i]._key);
|
|
132
|
-
if (!refs?.slide) continue;
|
|
133
|
-
const el = refs.slide;
|
|
134
|
-
const center = el.offsetTop + el.offsetHeight / 2;
|
|
135
|
-
slideData.push({ center, color: slideNavColors[i] || "" });
|
|
166
|
+
// Build ordered array of {center, color} for each slide — from cache when fresh
|
|
167
|
+
if (slideDataDirtyRef.current || !slideDataCacheRef.current) {
|
|
168
|
+
slideDataCacheRef.current = buildSlideData();
|
|
169
|
+
slideDataDirtyRef.current = false;
|
|
136
170
|
}
|
|
171
|
+
const slideData = slideDataCacheRef.current;
|
|
137
172
|
|
|
138
173
|
if (slideData.length === 0) return null;
|
|
139
174
|
|
|
@@ -170,7 +205,7 @@ export default function ParallaxGroupRenderer({
|
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
return null;
|
|
173
|
-
}, [slideNavColors, group.slides]);
|
|
208
|
+
}, [slideNavColors, group.slides, buildSlideData]);
|
|
174
209
|
|
|
175
210
|
// ── Capture page-level nav color on mount, restore on unmount ──
|
|
176
211
|
|