@morphika/andami 0.5.4 → 0.5.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/admin/assets/page.tsx +3 -2
- package/app/admin/layout.tsx +4 -0
- package/components/admin/nav-builder/NavBuilder.tsx +2 -1
- package/components/admin/styles/FontsEditor.tsx +2 -1
- package/components/builder/CoverSectionCanvas.tsx +7 -6
- package/components/builder/SettingsPanel.tsx +14 -8
- package/components/builder/SortableBlock.tsx +4 -0
- package/components/builder/SortableRow.tsx +2 -0
- package/components/builder/asset-browser/useR2Operations.ts +5 -4
- package/components/builder/editors/AudioBlockEditor.tsx +10 -8
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +10 -8
- package/components/builder/editors/ButtonBlockEditor.tsx +9 -7
- package/components/builder/editors/ImageBlockEditor.tsx +10 -8
- package/components/builder/editors/ImageGridBlockEditor.tsx +10 -8
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -4
- package/components/builder/editors/TextBlockEditor.tsx +471 -468
- package/components/builder/editors/VideoBlockEditor.tsx +10 -8
- package/components/builder/settings-panel/AnimationTab.tsx +11 -8
- package/components/builder/settings-panel/BlockLayoutTab.tsx +514 -511
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +2 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +11 -8
- package/components/builder/settings-panel/ColumnV2Settings.tsx +6 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +4 -3
- package/components/builder/settings-panel/CoverSectionSettings.tsx +14 -9
- package/components/builder/settings-panel/CustomSectionSettings.tsx +9 -7
- package/components/builder/settings-panel/PageSettings.tsx +39 -32
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +2 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +7 -5
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +13 -9
- package/components/builder/settings-panel/SectionV2Settings.tsx +7 -6
- package/components/builder/settings-panel/TRBLInputs.tsx +2 -2
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +16 -13
- package/components/ui/ToastStack.tsx +142 -0
- package/lib/auth-token.ts +5 -1
- package/lib/bot-guard.ts +6 -0
- package/lib/builder/constants.ts +0 -7
- package/lib/toast/index.ts +56 -0
- package/lib/toast/store.ts +56 -0
- package/lib/version.ts +1 -1
- package/package.json +3 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import type { RegisteredAsset, AssetRegistry, RelinkLogEntry } from "../../../lib/sanity/types";
|
|
5
5
|
import { formatDate, formatBytes } from "../../../lib/format-utils";
|
|
6
|
+
import { toast } from "../../../lib/toast";
|
|
6
7
|
|
|
7
8
|
function getStatusColor(status: string): string {
|
|
8
9
|
switch (status) {
|
|
@@ -83,8 +84,8 @@ export default function AdminAssetsPage() {
|
|
|
83
84
|
const data = await res.json();
|
|
84
85
|
if (!res.ok) throw new Error(data.error || "Scan failed");
|
|
85
86
|
await fetchRegistry();
|
|
86
|
-
|
|
87
|
-
`Scan complete: ${data.scanned_count} files
|
|
87
|
+
toast.success(
|
|
88
|
+
`Scan complete: ${data.scanned_count} files, ${data.new_assets} new, ${data.missing_assets} missing`
|
|
88
89
|
);
|
|
89
90
|
} catch (err) {
|
|
90
91
|
setError(err instanceof Error ? err.message : "Scan failed");
|
package/app/admin/layout.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { getSiteConfig } from "../../lib/config";
|
|
6
6
|
import { ANDAMI_VERSION } from "../../lib/version";
|
|
7
|
+
import ToastStack from "../../components/ui/ToastStack";
|
|
7
8
|
|
|
8
9
|
// ============================================
|
|
9
10
|
// Navigation Configuration — grouped by section
|
|
@@ -327,6 +328,9 @@ export default function AdminLayout({
|
|
|
327
328
|
{children}
|
|
328
329
|
</main>
|
|
329
330
|
</div>
|
|
331
|
+
|
|
332
|
+
{/* Global toast stack — mounted at admin shell so all admin pages get it */}
|
|
333
|
+
<ToastStack />
|
|
330
334
|
</div>
|
|
331
335
|
);
|
|
332
336
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useEffect } from "react";
|
|
4
4
|
import type { NavItem, NavDesign, PageListItem } from "../../../lib/sanity/types";
|
|
5
|
+
import { toast } from "../../../lib/toast";
|
|
5
6
|
import { migrateNavItems, validateLayout } from "./nav-builder-utils";
|
|
6
7
|
import NavBuilderGrid from "./NavBuilderGrid";
|
|
7
8
|
import NavLivePreview from "./NavLivePreview";
|
|
@@ -151,7 +152,7 @@ export default function NavBuilder({
|
|
|
151
152
|
const handleSave = useCallback(async () => {
|
|
152
153
|
const validation = validateLayout(items);
|
|
153
154
|
if (!validation.valid) {
|
|
154
|
-
|
|
155
|
+
toast.error(`Layout conflicts: ${validation.conflicts.join(", ")}`);
|
|
155
156
|
return;
|
|
156
157
|
}
|
|
157
158
|
await onSave(items, design);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
|
+
import { toast } from "../../../lib/toast";
|
|
5
6
|
import type { FontFamily, FontVariant } from "../../../lib/sanity/types";
|
|
6
7
|
import { Section, SaveButton } from "./shared";
|
|
7
8
|
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
@@ -73,7 +74,7 @@ export function FontsEditor({ fonts, onSave, saving }: { fonts: FontFamily[]; on
|
|
|
73
74
|
)
|
|
74
75
|
);
|
|
75
76
|
} catch (err) {
|
|
76
|
-
|
|
77
|
+
toast.error(err instanceof Error ? err.message : "Upload failed");
|
|
77
78
|
} finally {
|
|
78
79
|
setUploading(false);
|
|
79
80
|
}
|
|
@@ -60,9 +60,10 @@ export default function CoverSectionCanvas({
|
|
|
60
60
|
section,
|
|
61
61
|
onAddBlockTarget,
|
|
62
62
|
}: CoverSectionCanvasProps) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
63
|
+
const storeActiveViewport = useBuilderStore((s) => s.activeViewport);
|
|
64
|
+
const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
|
|
65
|
+
const addColumnV2 = useBuilderStore((s) => s.addColumnV2);
|
|
66
|
+
const activeViewport = storeActiveViewport || "desktop";
|
|
66
67
|
const [isSectionHovered, setIsSectionHovered] = useState(false);
|
|
67
68
|
|
|
68
69
|
const vhPixels = DEVICE_HEIGHTS[activeViewport];
|
|
@@ -182,12 +183,12 @@ export default function CoverSectionCanvas({
|
|
|
182
183
|
// store action (it writes to any section by _key); the runtime shapes align
|
|
183
184
|
// on `{ tablet?, phone? }` — the extra `cover_rows` field on CoverSection
|
|
184
185
|
// is carried through without touching the V2-shaped write path.
|
|
185
|
-
|
|
186
|
+
updateSectionV2Responsive(
|
|
186
187
|
section._key,
|
|
187
188
|
finalResponsive as PageSectionV2["responsive"],
|
|
188
189
|
);
|
|
189
190
|
},
|
|
190
|
-
[section,
|
|
191
|
+
[section, updateSectionV2Responsive],
|
|
191
192
|
);
|
|
192
193
|
|
|
193
194
|
return (
|
|
@@ -323,7 +324,7 @@ export default function CoverSectionCanvas({
|
|
|
323
324
|
<button
|
|
324
325
|
onClick={(e) => {
|
|
325
326
|
e.stopPropagation();
|
|
326
|
-
|
|
327
|
+
addColumnV2(section._key, rowNumber, 1, section.settings.grid_columns || 12);
|
|
327
328
|
}}
|
|
328
329
|
className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
|
|
329
330
|
style={{
|
|
@@ -46,7 +46,13 @@ import { BubbleTooltip } from "./BubbleIcons";
|
|
|
46
46
|
type SettingsTab = "settings" | "layout" | "seo" | "animation";
|
|
47
47
|
|
|
48
48
|
export default function SettingsPanel() {
|
|
49
|
-
const
|
|
49
|
+
const selectedRowKey = useBuilderStore((s) => s.selectedRowKey);
|
|
50
|
+
const selectedColumnKey = useBuilderStore((s) => s.selectedColumnKey);
|
|
51
|
+
const selectedBlockKey = useBuilderStore((s) => s.selectedBlockKey);
|
|
52
|
+
const editorMode = useBuilderStore((s) => s.editorMode);
|
|
53
|
+
const deleteBlock = useBuilderStore((s) => s.deleteBlock);
|
|
54
|
+
const deleteColumnV2 = useBuilderStore((s) => s.deleteColumnV2);
|
|
55
|
+
const deleteSection = useBuilderStore((s) => s.deleteSection);
|
|
50
56
|
const [activeTab, setActiveTab] = useState<SettingsTab>("settings");
|
|
51
57
|
|
|
52
58
|
const sel = useSettingsPanelSelection();
|
|
@@ -68,7 +74,7 @@ export default function SettingsPanel() {
|
|
|
68
74
|
} = sel;
|
|
69
75
|
|
|
70
76
|
// Reset to "settings" tab when selection changes
|
|
71
|
-
const selectionKey = `${
|
|
77
|
+
const selectionKey = `${selectedRowKey}-${selectedColumnKey}-${selectedBlockKey}`;
|
|
72
78
|
const prevSelectionKey = useRef(selectionKey);
|
|
73
79
|
useEffect(() => {
|
|
74
80
|
if (prevSelectionKey.current !== selectionKey) {
|
|
@@ -115,19 +121,19 @@ export default function SettingsPanel() {
|
|
|
115
121
|
let deleteTitle = "";
|
|
116
122
|
|
|
117
123
|
if (selectedBlock && !selectedBlock.isSection) {
|
|
118
|
-
onDelete = () =>
|
|
124
|
+
onDelete = () => deleteBlock(selectedBlock.block._key);
|
|
119
125
|
deleteTitle = "Delete";
|
|
120
126
|
} else if (selectedColumnV2 && effectiveSectionV2) {
|
|
121
|
-
onDelete = () =>
|
|
127
|
+
onDelete = () => deleteColumnV2(effectiveSectionV2._key, selectedColumnV2._key);
|
|
122
128
|
deleteTitle = "Delete Column";
|
|
123
129
|
} else if (selectedParallaxGroup && !selectedParallaxSlide) {
|
|
124
|
-
onDelete = () =>
|
|
130
|
+
onDelete = () => deleteSection(selectedParallaxGroup._key);
|
|
125
131
|
deleteTitle = "Delete Parallax Group";
|
|
126
132
|
} else if (selectedCoverSection) {
|
|
127
|
-
onDelete = () =>
|
|
133
|
+
onDelete = () => deleteSection(selectedCoverSection._key);
|
|
128
134
|
deleteTitle = "Delete Cover Section";
|
|
129
135
|
} else if (selectedSectionV2) {
|
|
130
|
-
onDelete = () =>
|
|
136
|
+
onDelete = () => deleteSection(selectedSectionV2._key);
|
|
131
137
|
deleteTitle = "Delete Section";
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -215,7 +221,7 @@ export default function SettingsPanel() {
|
|
|
215
221
|
if (animIdx >= 0) tabs.splice(animIdx, 1);
|
|
216
222
|
}
|
|
217
223
|
// Custom section editor mode: only Settings (Layout & Animation are per-instance on the parent page)
|
|
218
|
-
if (
|
|
224
|
+
if (editorMode === "customSection") {
|
|
219
225
|
const layoutIdx = tabs.findIndex((t) => t.id === "layout");
|
|
220
226
|
if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
|
|
221
227
|
const animIdx = tabs.findIndex((t) => t.id === "animation");
|
|
@@ -111,6 +111,8 @@ export default function SortableBlock({
|
|
|
111
111
|
ref={setNodeRef}
|
|
112
112
|
style={style}
|
|
113
113
|
className="relative"
|
|
114
|
+
aria-roledescription="Draggable block"
|
|
115
|
+
aria-label={info?.label || block._type}
|
|
114
116
|
onClick={(e) => { e.stopPropagation(); onSelect(); }}
|
|
115
117
|
>
|
|
116
118
|
<BlockLivePreview block={block} viewport={activeViewport} editable />
|
|
@@ -128,6 +130,8 @@ export default function SortableBlock({
|
|
|
128
130
|
? "ring-2 ring-[#3580f9] ring-offset-1 ring-offset-transparent rounded"
|
|
129
131
|
: ""
|
|
130
132
|
}`}
|
|
133
|
+
aria-roledescription="Draggable block"
|
|
134
|
+
aria-label={info?.label || block._type}
|
|
131
135
|
onClick={(e) => {
|
|
132
136
|
e.stopPropagation();
|
|
133
137
|
// If user clicked inside a contentEditable element (inline text editing),
|
|
@@ -232,6 +232,8 @@ export default function SortableRow({
|
|
|
232
232
|
className={`relative transition-[opacity,box-shadow] ${
|
|
233
233
|
isDragging ? "ring-2 ring-[#93278f] ring-offset-2 ring-offset-[#0a0a0a]" : ""
|
|
234
234
|
}`}
|
|
235
|
+
aria-roledescription="Draggable section"
|
|
236
|
+
aria-label={sectionLabel || "Section"}
|
|
235
237
|
onClick={(e) => {
|
|
236
238
|
e.stopPropagation();
|
|
237
239
|
if (isLockedSection) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useCallback, useRef } from "react";
|
|
4
4
|
import type { RegisteredAsset } from "../../../lib/sanity/types";
|
|
5
5
|
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
6
|
+
import { toast } from "../../../lib/toast";
|
|
6
7
|
import type { FolderNode } from "./types";
|
|
7
8
|
|
|
8
9
|
// ============================================
|
|
@@ -80,10 +81,10 @@ export function useR2Operations({
|
|
|
80
81
|
});
|
|
81
82
|
if (!res.ok) {
|
|
82
83
|
const data = await res.json().catch(() => ({}));
|
|
83
|
-
|
|
84
|
+
toast.error(data.error || "Delete failed");
|
|
84
85
|
}
|
|
85
86
|
} catch {
|
|
86
|
-
|
|
87
|
+
toast.error("Delete failed — network error");
|
|
87
88
|
} finally {
|
|
88
89
|
setActionLoading(false);
|
|
89
90
|
onRetry();
|
|
@@ -125,10 +126,10 @@ export function useR2Operations({
|
|
|
125
126
|
|
|
126
127
|
if (!res.ok) {
|
|
127
128
|
const data = await res.json().catch(() => ({}));
|
|
128
|
-
|
|
129
|
+
toast.error(data.error || "Rename failed");
|
|
129
130
|
}
|
|
130
131
|
} catch {
|
|
131
|
-
|
|
132
|
+
toast.error("Rename failed — network error");
|
|
132
133
|
} finally {
|
|
133
134
|
setRenameTarget(null);
|
|
134
135
|
setRenameValue("");
|
|
@@ -28,32 +28,34 @@ interface Props {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export default function AudioBlockEditor({ block }: Props) {
|
|
31
|
-
const
|
|
31
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
32
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
33
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
32
34
|
const viewport = useActiveViewport();
|
|
33
35
|
const paletteSwatches = usePaletteSwatches();
|
|
34
36
|
|
|
35
|
-
const snapshotOnFocus = () =>
|
|
37
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
36
38
|
|
|
37
39
|
const updateResponsive = (property: string, value: unknown) => {
|
|
38
40
|
if (viewport === "desktop") {
|
|
39
|
-
|
|
41
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
40
42
|
} else {
|
|
41
43
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
42
|
-
|
|
44
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
const resetOverride = (property: string) => {
|
|
47
49
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
48
|
-
|
|
50
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
const update = (updates: Partial<AudioBlock>) => {
|
|
52
|
-
|
|
54
|
+
updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
const updateDebounced = (updates: Partial<AudioBlock>) => {
|
|
56
|
-
|
|
58
|
+
updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
const effectiveWidth = getEffectiveValue<string>(
|
|
@@ -181,7 +183,7 @@ export default function AudioBlockEditor({ block }: Props) {
|
|
|
181
183
|
value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
182
184
|
onFocus={snapshotOnFocus}
|
|
183
185
|
onChange={(e) => {
|
|
184
|
-
|
|
186
|
+
_pushSnapshot();
|
|
185
187
|
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
186
188
|
}}
|
|
187
189
|
className={INPUT_CLASS}
|
|
@@ -29,32 +29,34 @@ interface Props {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export default function BeforeAfterBlockEditor({ block }: Props) {
|
|
32
|
-
const
|
|
32
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
33
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
34
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
33
35
|
const viewport = useActiveViewport();
|
|
34
36
|
const paletteSwatches = usePaletteSwatches();
|
|
35
37
|
|
|
36
|
-
const snapshotOnFocus = () =>
|
|
38
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
37
39
|
|
|
38
40
|
const updateResponsive = (property: string, value: unknown) => {
|
|
39
41
|
if (viewport === "desktop") {
|
|
40
|
-
|
|
42
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
41
43
|
} else {
|
|
42
44
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
43
|
-
|
|
45
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
const resetOverride = (property: string) => {
|
|
48
50
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
49
|
-
|
|
51
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
const update = (updates: Partial<BeforeAfterBlock>) => {
|
|
53
|
-
|
|
55
|
+
updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
const updateDebounced = (updates: Partial<BeforeAfterBlock>) => {
|
|
57
|
-
|
|
59
|
+
updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const beforeType = block.before_media_type || "image";
|
|
@@ -297,7 +299,7 @@ export default function BeforeAfterBlockEditor({ block }: Props) {
|
|
|
297
299
|
value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
298
300
|
onFocus={snapshotOnFocus}
|
|
299
301
|
onChange={(e) => {
|
|
300
|
-
|
|
302
|
+
_pushSnapshot();
|
|
301
303
|
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
302
304
|
}}
|
|
303
305
|
className={INPUT_CLASS}
|
|
@@ -23,32 +23,34 @@ interface Props {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export default function ButtonBlockEditor({ block }: Props) {
|
|
26
|
-
const
|
|
26
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
27
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
28
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
27
29
|
const viewport = useActiveViewport();
|
|
28
|
-
const snapshotOnFocus = () =>
|
|
30
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
29
31
|
|
|
30
32
|
// Responsive-aware update
|
|
31
33
|
const updateResponsive = (property: string, value: unknown) => {
|
|
32
34
|
if (viewport === "desktop") {
|
|
33
|
-
|
|
35
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
34
36
|
} else {
|
|
35
37
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
36
|
-
|
|
38
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
37
39
|
}
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
const resetOverride = (property: string) => {
|
|
41
43
|
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
42
|
-
|
|
44
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
// Direct update (base block, not responsive)
|
|
46
48
|
const update = (updates: Partial<ButtonBlock>) => {
|
|
47
|
-
|
|
49
|
+
updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
const updateDebounced = (updates: Partial<ButtonBlock>) => {
|
|
51
|
-
|
|
53
|
+
updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
// Effective values for the active viewport
|
|
@@ -25,33 +25,35 @@ interface Props {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export default function ImageBlockEditor({ block }: Props) {
|
|
28
|
-
const
|
|
28
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
29
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
30
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
29
31
|
const viewport = useActiveViewport();
|
|
30
32
|
|
|
31
|
-
const snapshotOnFocus = () =>
|
|
33
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
32
34
|
|
|
33
35
|
// Responsive-aware update for layout/appearance properties
|
|
34
36
|
const updateResponsive = (property: string, value: unknown) => {
|
|
35
37
|
if (viewport === "desktop") {
|
|
36
|
-
|
|
38
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
37
39
|
} else {
|
|
38
40
|
const overrides = setResponsiveOverride(block, viewport, property, value);
|
|
39
|
-
|
|
41
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
const resetOverride = (property: string) => {
|
|
44
46
|
const overrides = setResponsiveOverride(block, viewport, property, undefined);
|
|
45
|
-
|
|
47
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
// Direct update (base block, not responsive)
|
|
49
51
|
const update = (updates: Partial<ImageBlock>) => {
|
|
50
|
-
|
|
52
|
+
updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const updateDebounced = (updates: Partial<ImageBlock>) => {
|
|
54
|
-
|
|
56
|
+
updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
// Effective values for the active viewport
|
|
@@ -166,7 +168,7 @@ export default function ImageBlockEditor({ block }: Props) {
|
|
|
166
168
|
value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
167
169
|
onFocus={snapshotOnFocus}
|
|
168
170
|
onChange={(e) => {
|
|
169
|
-
|
|
171
|
+
_pushSnapshot();
|
|
170
172
|
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
171
173
|
}}
|
|
172
174
|
className={INPUT_CLASS}
|
|
@@ -95,35 +95,37 @@ function ImageThumb({
|
|
|
95
95
|
// ============================================
|
|
96
96
|
|
|
97
97
|
export default function ImageGridBlockEditor({ block }: Props) {
|
|
98
|
-
const
|
|
98
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
99
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
100
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
99
101
|
const viewport = useActiveViewport();
|
|
100
102
|
const images = block.images || [];
|
|
101
103
|
const [browserOpen, setBrowserOpen] = useState(false);
|
|
102
104
|
|
|
103
|
-
const snapshotOnFocus = () =>
|
|
105
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
104
106
|
|
|
105
107
|
// Responsive update helper
|
|
106
108
|
const updateResponsive = (property: string, value: unknown) => {
|
|
107
109
|
if (viewport === "desktop") {
|
|
108
|
-
|
|
110
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
109
111
|
} else {
|
|
110
112
|
const overrides = setResponsiveOverride(block, viewport, property, value);
|
|
111
|
-
|
|
113
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
112
114
|
}
|
|
113
115
|
};
|
|
114
116
|
|
|
115
117
|
const resetOverride = (property: string) => {
|
|
116
118
|
const overrides = setResponsiveOverride(block, viewport, property, undefined);
|
|
117
|
-
|
|
119
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
118
120
|
};
|
|
119
121
|
|
|
120
122
|
// Direct update (always base block — for images content)
|
|
121
123
|
const update = (updates: Partial<ImageGridBlock>) => {
|
|
122
|
-
|
|
124
|
+
updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
123
125
|
};
|
|
124
126
|
|
|
125
127
|
const updateDebounced = (updates: Partial<ImageGridBlock>) => {
|
|
126
|
-
|
|
128
|
+
updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
127
129
|
};
|
|
128
130
|
|
|
129
131
|
// Add multiple images at once from browser
|
|
@@ -378,7 +380,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
|
|
|
378
380
|
value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
379
381
|
onFocus={snapshotOnFocus}
|
|
380
382
|
onChange={(v) => {
|
|
381
|
-
|
|
383
|
+
_pushSnapshot();
|
|
382
384
|
updateResponsive("border_radius", v.replace(/[^0-9]/g, ""));
|
|
383
385
|
}}
|
|
384
386
|
placeholder="0"
|
|
@@ -39,21 +39,21 @@ export function getSpacerPx(block: SpacerBlock): number {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export default function SpacerBlockEditor({ block }: Props) {
|
|
42
|
-
const
|
|
42
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
43
43
|
const viewport = useActiveViewport();
|
|
44
44
|
|
|
45
45
|
const updateResponsive = (property: string, value: unknown) => {
|
|
46
46
|
if (viewport === "desktop") {
|
|
47
|
-
|
|
47
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
48
48
|
} else {
|
|
49
49
|
const overrides = setResponsiveOverride(block, viewport, property, value);
|
|
50
|
-
|
|
50
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const resetOverride = (property: string) => {
|
|
55
55
|
const overrides = setResponsiveOverride(block, viewport, property, undefined);
|
|
56
|
-
|
|
56
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const effectiveHeight = getEffectiveValue<string>(
|