@morphika/andami 0.1.3 → 0.1.5
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/(site)/[slug]/page.tsx +2 -2
- package/app/(site)/layout.tsx +1 -0
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +2 -2
- package/app/admin/layout.tsx +2 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/settings/route.ts +40 -15
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +6 -3
- package/components/admin/index.ts +7 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
- package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
- package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
- package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
- package/components/admin/nav-builder/index.ts +2 -0
- package/components/blocks/BlockRenderer.tsx +65 -13
- package/components/blocks/ButtonBlockRenderer.tsx +29 -6
- package/components/blocks/CoverBlockRenderer.tsx +36 -14
- package/components/blocks/ImageBlockRenderer.tsx +5 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
- package/components/blocks/PageRenderer.tsx +4 -2
- package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
- package/components/blocks/SectionRenderer.tsx +9 -8
- package/components/blocks/SectionV2Renderer.tsx +8 -8
- package/components/blocks/SpacerBlockRenderer.tsx +4 -2
- package/components/blocks/TextBlockRenderer.tsx +9 -4
- package/components/builder/BuilderCanvas.tsx +10 -4
- package/components/builder/ColorPicker.tsx +51 -243
- package/components/builder/ColorSwatchPicker.tsx +214 -274
- package/components/builder/DndWrapper.tsx +5 -2
- package/components/builder/SectionV2Canvas.tsx +15 -4
- package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
- package/components/builder/color-picker/AlphaSlider.tsx +141 -0
- package/components/builder/color-picker/AngleControl.tsx +138 -0
- package/components/builder/color-picker/ColorInputs.tsx +105 -0
- package/components/builder/color-picker/EyedropperButton.tsx +74 -0
- package/components/builder/color-picker/GradientBar.tsx +222 -0
- package/components/builder/color-picker/GradientPreview.tsx +53 -0
- package/components/builder/color-picker/HueSlider.tsx +124 -0
- package/components/builder/color-picker/MeshCanvas.tsx +172 -0
- package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
- package/components/builder/color-picker/MeshPointList.tsx +200 -0
- package/components/builder/color-picker/PositionControl.tsx +158 -0
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
- package/components/builder/color-picker/StopEditor.tsx +178 -0
- package/components/builder/color-picker/SwatchBar.tsx +93 -0
- package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
- package/components/builder/color-picker/index.ts +62 -0
- package/components/builder/color-picker/types.ts +115 -0
- package/components/builder/color-picker/utils.ts +138 -0
- package/components/builder/editors/CoverBlockEditor.tsx +86 -32
- package/components/builder/editors/ProjectGridEditor.tsx +51 -4
- package/components/builder/hooks/useColumnDrag.ts +25 -27
- package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
- package/components/builder/settings-panel/LayoutTab.tsx +382 -310
- package/components/builder/settings-panel/PageSettings.tsx +6 -4
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
- package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
- package/components/ui/Navbar.tsx +95 -25
- package/components/ui/PortfolioTracker.tsx +3 -3
- package/lib/assets.ts +1 -1
- package/lib/auth.ts +1 -1
- package/lib/builder/gradient-presets.ts +128 -0
- package/lib/builder/layout-styles.ts +16 -10
- package/lib/builder/serializer.ts +1 -0
- package/lib/builder/store-blocks.ts +48 -61
- package/lib/builder/store-helpers.ts +31 -14
- package/lib/builder/store.ts +59 -41
- package/lib/builder/types.ts +14 -0
- package/lib/color-utils.ts +200 -0
- package/lib/revalidate.ts +2 -2
- package/lib/sanity/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- package/package.json +8 -2
- package/sanity/schemas/siteSettings.ts +34 -0
- package/styles/base.css +3 -3
- package/app/globals.css +0 -7
|
@@ -1,274 +1,214 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ColorSwatchPicker —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
inline
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{label}
|
|
216
|
-
</label>
|
|
217
|
-
)}
|
|
218
|
-
{trigger}
|
|
219
|
-
{open && typeof document !== "undefined" &&
|
|
220
|
-
createPortal(
|
|
221
|
-
<div
|
|
222
|
-
ref={portalRef}
|
|
223
|
-
className="fixed z-[9999]"
|
|
224
|
-
style={{ top: dropdownPos.top, left: dropdownPos.left }}
|
|
225
|
-
>
|
|
226
|
-
{panel}
|
|
227
|
-
</div>,
|
|
228
|
-
document.body
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ─── Hook: fetch palette swatches from admin styles API ───
|
|
236
|
-
|
|
237
|
-
let cachedSwatches: ColorSwatch[] | null = null;
|
|
238
|
-
let cachePromise: Promise<ColorSwatch[]> | null = null;
|
|
239
|
-
|
|
240
|
-
export function usePaletteSwatches(): ColorSwatch[] {
|
|
241
|
-
const [swatches, setSwatches] = useState<ColorSwatch[]>(cachedSwatches || []);
|
|
242
|
-
|
|
243
|
-
useEffect(() => {
|
|
244
|
-
if (cachedSwatches) {
|
|
245
|
-
setSwatches(cachedSwatches);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (!cachePromise) {
|
|
250
|
-
cachePromise = fetch("/api/admin/styles")
|
|
251
|
-
.then((res) => (res.ok ? res.json() : { styles: { colors: { swatches: [] } } }))
|
|
252
|
-
.then((data) => {
|
|
253
|
-
const s = data?.styles?.colors?.swatches || [];
|
|
254
|
-
cachedSwatches = s;
|
|
255
|
-
return s;
|
|
256
|
-
})
|
|
257
|
-
.catch(() => {
|
|
258
|
-
/* Color palette unavailable — use empty swatches */
|
|
259
|
-
cachedSwatches = [];
|
|
260
|
-
return [];
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
cachePromise.then((s) => setSwatches(s));
|
|
265
|
-
}, []);
|
|
266
|
-
|
|
267
|
-
return swatches;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Invalidate the cached swatches (call after saving palette) */
|
|
271
|
-
export function invalidatePaletteCache() {
|
|
272
|
-
cachedSwatches = null;
|
|
273
|
-
cachePromise = null;
|
|
274
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColorSwatchPicker — Trigger component for the color picker.
|
|
5
|
+
*
|
|
6
|
+
* Shows a colored swatch + hex/gradient label. On click, opens the
|
|
7
|
+
* UnifiedColorPicker as a centered modal (Color Picker v2).
|
|
8
|
+
*
|
|
9
|
+
* Phase 3: Supports `ColorField` (string | GradientValue) for value/onChange.
|
|
10
|
+
* - When `allowGradients={true}`, gradient tabs appear in the modal.
|
|
11
|
+
* - The trigger swatch renders gradients as a mini CSS preview.
|
|
12
|
+
* - Backward compatible: string-only usage still works (default behavior).
|
|
13
|
+
*
|
|
14
|
+
* Used in: SettingsPanel (row/block bg, border), TextBlockEditor (text color),
|
|
15
|
+
* CoverBlockEditor (text color), BlockLayoutTab, SectionV2LayoutTab,
|
|
16
|
+
* PageSettings, ParallaxSlideSettings, and any future color field.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState, useCallback, useEffect } from "react";
|
|
20
|
+
import { UnifiedColorPicker } from "./color-picker";
|
|
21
|
+
import {
|
|
22
|
+
isValidHex,
|
|
23
|
+
isGradient,
|
|
24
|
+
colorToCSS,
|
|
25
|
+
resolveColorHex,
|
|
26
|
+
serializeColorField,
|
|
27
|
+
} from "../../lib/color-utils";
|
|
28
|
+
import type { ColorSwatch, ColorField } from "../../lib/sanity/types";
|
|
29
|
+
|
|
30
|
+
interface ColorSwatchPickerProps {
|
|
31
|
+
/** Current color value (hex string, empty string, or GradientValue) */
|
|
32
|
+
value: ColorField | string;
|
|
33
|
+
/** Callback when color changes */
|
|
34
|
+
onChange: (value: ColorField) => void;
|
|
35
|
+
/** Palette swatches from global styles */
|
|
36
|
+
swatches?: ColorSwatch[];
|
|
37
|
+
/** Optional label */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Allow clearing the color */
|
|
40
|
+
allowClear?: boolean;
|
|
41
|
+
/** Allow gradient tabs in the picker (default false) */
|
|
42
|
+
allowGradients?: boolean;
|
|
43
|
+
/** Live preview callback — called during drag in the picker (Phase 4). */
|
|
44
|
+
onPreview?: (value: ColorField) => void;
|
|
45
|
+
/** @deprecated Inline mode is no longer supported in v2. Ignored. */
|
|
46
|
+
inline?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function ColorSwatchPicker({
|
|
50
|
+
value,
|
|
51
|
+
onChange,
|
|
52
|
+
swatches = [],
|
|
53
|
+
label,
|
|
54
|
+
allowClear = true,
|
|
55
|
+
allowGradients = false,
|
|
56
|
+
onPreview,
|
|
57
|
+
}: ColorSwatchPickerProps) {
|
|
58
|
+
const [open, setOpen] = useState(false);
|
|
59
|
+
|
|
60
|
+
const handleOpen = useCallback(() => {
|
|
61
|
+
setOpen(true);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleClose = useCallback(() => {
|
|
65
|
+
setOpen(false);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const handleChange = useCallback(
|
|
69
|
+
(newValue: ColorField) => {
|
|
70
|
+
onChange(newValue);
|
|
71
|
+
},
|
|
72
|
+
[onChange]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleClear = useCallback(() => {
|
|
76
|
+
onChange("");
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}, [onChange]);
|
|
79
|
+
|
|
80
|
+
// Determine display values for the trigger
|
|
81
|
+
const isGrad = typeof value !== "string" && isGradient(value as ColorField);
|
|
82
|
+
const displayHex = isGrad
|
|
83
|
+
? resolveColorHex(value as ColorField)
|
|
84
|
+
: typeof value === "string"
|
|
85
|
+
? value
|
|
86
|
+
: "";
|
|
87
|
+
const displayLabel = isGrad
|
|
88
|
+
? (value as { type: string }).type.charAt(0).toUpperCase() +
|
|
89
|
+
(value as { type: string }).type.slice(1)
|
|
90
|
+
: displayHex
|
|
91
|
+
? displayHex.toUpperCase()
|
|
92
|
+
: "None";
|
|
93
|
+
|
|
94
|
+
// Build trigger swatch style
|
|
95
|
+
const swatchStyle: React.CSSProperties = isGrad
|
|
96
|
+
? {
|
|
97
|
+
backgroundImage: colorToCSS(value as ColorField),
|
|
98
|
+
}
|
|
99
|
+
: {
|
|
100
|
+
background:
|
|
101
|
+
typeof value === "string" && value && isValidHex(value)
|
|
102
|
+
? value
|
|
103
|
+
: "transparent",
|
|
104
|
+
backgroundImage:
|
|
105
|
+
!value
|
|
106
|
+
? "linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%), linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%)"
|
|
107
|
+
: undefined,
|
|
108
|
+
backgroundSize: !value ? "6px 6px" : undefined,
|
|
109
|
+
backgroundPosition: !value ? "0 0, 3px 3px" : undefined,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Picker value: ensure we pass a valid ColorField
|
|
113
|
+
const pickerValue: ColorField =
|
|
114
|
+
isGrad
|
|
115
|
+
? (value as ColorField)
|
|
116
|
+
: typeof value === "string" && isValidHex(value)
|
|
117
|
+
? value
|
|
118
|
+
: "#ffffff";
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="relative">
|
|
122
|
+
{label && (
|
|
123
|
+
<label className="text-[10px] text-neutral-500 uppercase tracking-wider block mb-1">
|
|
124
|
+
{label}
|
|
125
|
+
</label>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Trigger button */}
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={handleOpen}
|
|
132
|
+
className="flex items-center gap-2 px-1.5 py-1 rounded-lg border border-neutral-200 bg-white hover:border-neutral-300 transition-colors cursor-pointer w-full"
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
className="w-6 h-6 rounded-md border border-neutral-200 shrink-0"
|
|
136
|
+
style={swatchStyle}
|
|
137
|
+
/>
|
|
138
|
+
<span className="text-[11px] text-neutral-500 font-mono truncate">
|
|
139
|
+
{displayLabel}
|
|
140
|
+
</span>
|
|
141
|
+
</button>
|
|
142
|
+
|
|
143
|
+
{/* Clear button (inline, below trigger) */}
|
|
144
|
+
{allowClear && value && (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={handleClear}
|
|
148
|
+
className="w-full mt-1 py-1 rounded-md border border-neutral-200 text-neutral-400 text-[9px] uppercase tracking-widest cursor-pointer hover:border-red-300 hover:text-red-500 transition-colors"
|
|
149
|
+
>
|
|
150
|
+
Clear
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Modal picker */}
|
|
155
|
+
{open && (
|
|
156
|
+
<UnifiedColorPicker
|
|
157
|
+
value={pickerValue}
|
|
158
|
+
onChange={handleChange}
|
|
159
|
+
onClose={handleClose}
|
|
160
|
+
swatches={swatches}
|
|
161
|
+
confirmLabel="Apply Color"
|
|
162
|
+
allowGradients={allowGradients}
|
|
163
|
+
onPreview={onPreview}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Hook: fetch palette swatches from admin styles API ───
|
|
171
|
+
|
|
172
|
+
let cachedSwatches: ColorSwatch[] | null = null;
|
|
173
|
+
let cachePromise: Promise<ColorSwatch[]> | null = null;
|
|
174
|
+
|
|
175
|
+
export function usePaletteSwatches(): ColorSwatch[] {
|
|
176
|
+
const [swatches, setSwatches] = useState<ColorSwatch[]>(
|
|
177
|
+
cachedSwatches || []
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (cachedSwatches) {
|
|
182
|
+
setSwatches(cachedSwatches);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!cachePromise) {
|
|
187
|
+
cachePromise = fetch("/api/admin/styles")
|
|
188
|
+
.then((res) =>
|
|
189
|
+
res.ok
|
|
190
|
+
? res.json()
|
|
191
|
+
: { styles: { colors: { swatches: [] } } }
|
|
192
|
+
)
|
|
193
|
+
.then((data) => {
|
|
194
|
+
const s = data?.styles?.colors?.swatches || [];
|
|
195
|
+
cachedSwatches = s;
|
|
196
|
+
return s;
|
|
197
|
+
})
|
|
198
|
+
.catch(() => {
|
|
199
|
+
cachedSwatches = [];
|
|
200
|
+
return [];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cachePromise.then((s) => setSwatches(s));
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
return swatches;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Invalidate the cached swatches (call after saving palette) */
|
|
211
|
+
export function invalidatePaletteCache() {
|
|
212
|
+
cachedSwatches = null;
|
|
213
|
+
cachePromise = null;
|
|
214
|
+
}
|
|
@@ -65,7 +65,10 @@ function parseId(id: string) {
|
|
|
65
65
|
// Drag Overlay Components
|
|
66
66
|
// ============================================
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// PERF-001 fix: Memoize RowDragOverlay — subscribes to s.rows which changes
|
|
69
|
+
// on any content mutation. Without memo, DragOverlay re-renders on every
|
|
70
|
+
// zoom/pan change because DndWrapper re-renders (canvasZoom subscription).
|
|
71
|
+
const RowDragOverlay = memo(function RowDragOverlay({ rowKey }: { rowKey: string }) {
|
|
69
72
|
const rows = useBuilderStore((s) => s.rows);
|
|
70
73
|
const item = rows.find((r) => r._key === rowKey);
|
|
71
74
|
if (!item) return null;
|
|
@@ -114,7 +117,7 @@ function RowDragOverlay({ rowKey }: { rowKey: string }) {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
return null;
|
|
117
|
-
}
|
|
120
|
+
});
|
|
118
121
|
|
|
119
122
|
const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: { blockKey: string; rowKey: string }) {
|
|
120
123
|
const rows = useBuilderStore((s) => s.rows);
|
|
@@ -101,18 +101,29 @@ export default function SectionV2Canvas({
|
|
|
101
101
|
// Measure container width for pixel position calculation.
|
|
102
102
|
// Uses ResizeObserver so it updates when the grid resizes (zoom change, window resize)
|
|
103
103
|
// and is also immediately available when a drag starts (no stale 0 value).
|
|
104
|
+
// PERF-003 fix: Debounce ResizeObserver callback to avoid excessive re-renders
|
|
105
|
+
// during continuous resize events (zoom drag, window resize).
|
|
104
106
|
useEffect(() => {
|
|
105
107
|
const el = gridContainerRef.current;
|
|
106
108
|
if (!el) return;
|
|
109
|
+
let rafId: number | null = null;
|
|
107
110
|
const ro = new ResizeObserver((entries) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
// Coalesce via rAF — at most one setState per frame
|
|
112
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
113
|
+
rafId = requestAnimationFrame(() => {
|
|
114
|
+
rafId = null;
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
setContainerWidth(entry.contentRect.width);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
111
119
|
});
|
|
112
120
|
ro.observe(el);
|
|
113
121
|
// Set initial value immediately
|
|
114
122
|
setContainerWidth(el.getBoundingClientRect().width);
|
|
115
|
-
return () =>
|
|
123
|
+
return () => {
|
|
124
|
+
ro.disconnect();
|
|
125
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
126
|
+
};
|
|
116
127
|
}, []);
|
|
117
128
|
|
|
118
129
|
// Use preview columns during resize, otherwise responsive-aware effective columns
|
|
@@ -23,10 +23,15 @@ export function useAssetBrowser(onScanComplete?: (result: Record<string, unknown
|
|
|
23
23
|
const [uploading, setUploading] = useState<UploadingFile[]>([]);
|
|
24
24
|
const [r2Available, setR2Available] = useState(false);
|
|
25
25
|
const uploadCleanupTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
26
|
+
// LEAK-001 fix: Mounted guard prevents setState calls from timer callbacks
|
|
27
|
+
// that fire during the unmount cleanup race window.
|
|
28
|
+
const isMountedRef = useRef(true);
|
|
26
29
|
|
|
27
30
|
// Clean up pending timer on unmount
|
|
28
31
|
useEffect(() => {
|
|
32
|
+
isMountedRef.current = true;
|
|
29
33
|
return () => {
|
|
34
|
+
isMountedRef.current = false;
|
|
30
35
|
if (uploadCleanupTimer.current) clearTimeout(uploadCleanupTimer.current);
|
|
31
36
|
};
|
|
32
37
|
}, []);
|
|
@@ -297,10 +302,13 @@ export function useAssetBrowser(onScanComplete?: (result: Record<string, unknown
|
|
|
297
302
|
onUploadComplete?.();
|
|
298
303
|
|
|
299
304
|
// Clear completed uploads after a short delay (with cleanup on unmount)
|
|
305
|
+
// LEAK-001 fix: Guard prevents setState after unmount.
|
|
300
306
|
if (uploadCleanupTimer.current) clearTimeout(uploadCleanupTimer.current);
|
|
301
307
|
uploadCleanupTimer.current = setTimeout(() => {
|
|
302
308
|
uploadCleanupTimer.current = null;
|
|
303
|
-
|
|
309
|
+
if (isMountedRef.current) {
|
|
310
|
+
setUploading((prev) => prev.filter((u) => u.status !== "done"));
|
|
311
|
+
}
|
|
304
312
|
}, 2000);
|
|
305
313
|
}, [fetchAssets, onUploadComplete]);
|
|
306
314
|
|