@morphika/andami 0.5.4 → 0.5.6
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/ColumnDragOverlay.tsx +4 -4
- package/components/builder/CoverSectionCanvas.tsx +10 -9
- package/components/builder/InsertionLines.tsx +3 -3
- package/components/builder/SectionV2Canvas.tsx +3 -3
- package/components/builder/SectionV2Column.tsx +20 -20
- 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/live-preview/drag-utils.tsx +5 -3
- 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 +10 -9
- 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 +5 -10
- 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
|
}
|
|
@@ -5,7 +5,7 @@ import { createPortal } from "react-dom";
|
|
|
5
5
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
6
6
|
import type { PageSectionV2, CoverSection, SectionColumn } from "../../lib/sanity/types";
|
|
7
7
|
import { isPageSectionV2, isCoverSection } from "../../lib/sanity/types";
|
|
8
|
-
import {
|
|
8
|
+
import { BUILDER_YELLOW } from "../../lib/builder/constants";
|
|
9
9
|
|
|
10
10
|
/** Color used when the current drop target is invalid (cross-section
|
|
11
11
|
* swap, or a target that is not V2/Cover). Red 500 with compatible
|
|
@@ -57,9 +57,9 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
|
57
57
|
|
|
58
58
|
const blockCount = (col.blocks || []).length;
|
|
59
59
|
|
|
60
|
-
// Pick accent based on drop validity.
|
|
61
|
-
const accentColor = isValidDrop ?
|
|
62
|
-
const accentRgb = isValidDrop ? "
|
|
60
|
+
// Pick accent based on drop validity. Column drag = BUILDER_YELLOW.
|
|
61
|
+
const accentColor = isValidDrop ? BUILDER_YELLOW : INVALID_RED;
|
|
62
|
+
const accentRgb = isValidDrop ? "214, 137, 0" : INVALID_RED_RGB;
|
|
63
63
|
|
|
64
64
|
const overlay = (
|
|
65
65
|
<div
|
|
@@ -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,14 +324,14 @@ 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={{
|
|
330
331
|
padding: "5px 16px",
|
|
331
|
-
background: `rgba(
|
|
332
|
-
color: "#
|
|
333
|
-
border: "1.5px dashed rgba(
|
|
332
|
+
background: `rgba(214, 137, 0, 0.10)`,
|
|
333
|
+
color: "#d68900",
|
|
334
|
+
border: "1.5px dashed rgba(214, 137, 0, 0.4)",
|
|
334
335
|
opacity: isSectionHovered ? 1 : 0,
|
|
335
336
|
pointerEvents: isSectionHovered ? "auto" : "none",
|
|
336
337
|
transition: "opacity 150ms",
|
|
@@ -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_YELLOW } from "../../lib/builder/constants";
|
|
7
7
|
|
|
8
8
|
// ============================================
|
|
9
9
|
// InsertionLines — Visual insertion indicators between adjacent columns
|
|
@@ -154,7 +154,7 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
154
154
|
bottom: 4,
|
|
155
155
|
width: isActive ? 4 : 0,
|
|
156
156
|
transform: "translateX(-50%)",
|
|
157
|
-
background:
|
|
157
|
+
background: BUILDER_YELLOW,
|
|
158
158
|
borderRadius: 2,
|
|
159
159
|
transition: "width 100ms ease-out, opacity 100ms ease-out",
|
|
160
160
|
opacity: isActive ? 1 : 0,
|
|
@@ -171,7 +171,7 @@ export const InsertionLines = memo(function InsertionLines({
|
|
|
171
171
|
width: 14,
|
|
172
172
|
height: 14,
|
|
173
173
|
borderRadius: "50%",
|
|
174
|
-
background:
|
|
174
|
+
background: BUILDER_YELLOW,
|
|
175
175
|
border: "2px solid white",
|
|
176
176
|
boxShadow: "0 1px 4px rgba(53, 128, 249, 0.4)",
|
|
177
177
|
display: "flex",
|
|
@@ -306,11 +306,11 @@ export default function SectionV2Canvas({
|
|
|
306
306
|
}}
|
|
307
307
|
className={`rounded-lg border-2 border-dashed text-xs font-medium transition-all flex items-center justify-center cursor-pointer ${
|
|
308
308
|
isGapTarget
|
|
309
|
-
? "border-
|
|
309
|
+
? "border-[#d68900] text-[#d68900] bg-[#d68900]/10 opacity-100"
|
|
310
310
|
: showAsDropTarget
|
|
311
|
-
? "border-
|
|
311
|
+
? "border-[#d68900]/40 text-[#d68900]/60 bg-[#d68900]/5 opacity-100"
|
|
312
312
|
: isSectionHovered
|
|
313
|
-
? "border-[#
|
|
313
|
+
? "border-[#d68900]/25 text-[#d68900]/50 hover:text-[#d68900] hover:border-[#d68900]/60 hover:bg-[#d68900]/5 opacity-100"
|
|
314
314
|
: "border-transparent text-transparent opacity-0 pointer-events-none"
|
|
315
315
|
}`}
|
|
316
316
|
>
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getBorderStyles,
|
|
16
16
|
} from "../../lib/builder/layout-styles";
|
|
17
17
|
import { isSectionBlockType } from "../../lib/builder/types";
|
|
18
|
-
import {
|
|
18
|
+
import { BUILDER_YELLOW, BUILDER_YELLOW_LIGHT } from "../../lib/builder/constants";
|
|
19
19
|
import { BubbleTooltip, CloseIcon, DragDropIcon } from "./BubbleIcons";
|
|
20
20
|
|
|
21
21
|
// ============================================
|
|
@@ -89,17 +89,17 @@ function ResizeHandle({
|
|
|
89
89
|
height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
|
|
90
90
|
borderRadius: isActive ? "50%" : 999,
|
|
91
91
|
backgroundColor: isActive
|
|
92
|
-
? "rgba(
|
|
92
|
+
? "rgba(214, 137, 0, 0.9)"
|
|
93
93
|
: isHoveredEdge
|
|
94
|
-
? "rgba(
|
|
94
|
+
? "rgba(214, 137, 0, 0.7)"
|
|
95
95
|
: showChrome
|
|
96
|
-
? "rgba(
|
|
97
|
-
: "rgba(
|
|
96
|
+
? "rgba(214, 137, 0, 0.5)"
|
|
97
|
+
: "rgba(214, 137, 0, 0.2)",
|
|
98
98
|
transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
|
|
99
99
|
boxShadow: isActive
|
|
100
|
-
? "0 0 10px rgba(
|
|
100
|
+
? "0 0 10px rgba(214, 137, 0, 0.5)"
|
|
101
101
|
: isHoveredEdge
|
|
102
|
-
? "0 0 6px rgba(
|
|
102
|
+
? "0 0 6px rgba(214, 137, 0, 0.2)"
|
|
103
103
|
: undefined,
|
|
104
104
|
}}
|
|
105
105
|
/>
|
|
@@ -304,15 +304,15 @@ export default function SectionV2Column({
|
|
|
304
304
|
style={{
|
|
305
305
|
transition: "box-shadow 150ms",
|
|
306
306
|
...(isSwapTarget
|
|
307
|
-
? { boxShadow: `inset 0 0 0 2px ${
|
|
307
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_YELLOW}`, background: "rgba(214, 137, 0, 0.08)" }
|
|
308
308
|
: isBlockOver
|
|
309
|
-
? { boxShadow: `inset 0 0 0 2px ${
|
|
309
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_YELLOW}` }
|
|
310
310
|
: isSelected
|
|
311
|
-
? { boxShadow: `inset 0 0 0 2px rgba(
|
|
311
|
+
? { boxShadow: `inset 0 0 0 2px rgba(214, 137, 0, 0.6)` }
|
|
312
312
|
: isHovered
|
|
313
|
-
? { boxShadow: `inset 0 0 0 1.5px rgba(
|
|
313
|
+
? { boxShadow: `inset 0 0 0 1.5px rgba(214, 137, 0, 0.5)` }
|
|
314
314
|
: showFaintOutline
|
|
315
|
-
? { boxShadow: `inset 0 0 0 1px rgba(
|
|
315
|
+
? { boxShadow: `inset 0 0 0 1px rgba(214, 137, 0, 0.2)`, borderRadius: 4 }
|
|
316
316
|
: undefined),
|
|
317
317
|
}}
|
|
318
318
|
/>
|
|
@@ -383,14 +383,14 @@ export default function SectionV2Column({
|
|
|
383
383
|
<div
|
|
384
384
|
className="flex items-center rounded-[5px]"
|
|
385
385
|
style={{
|
|
386
|
-
background:
|
|
387
|
-
border:
|
|
386
|
+
background: BUILDER_YELLOW_LIGHT,
|
|
387
|
+
border: `1.5px solid ${BUILDER_YELLOW}`,
|
|
388
388
|
}}
|
|
389
389
|
>
|
|
390
390
|
{/* Drag handle — starts a column drag via @dnd-kit, selects on click */}
|
|
391
391
|
<button
|
|
392
392
|
type="button"
|
|
393
|
-
className="group/bb relative text-[#
|
|
393
|
+
className="group/bb relative text-[#d68900] hover:bg-[#d68900]/15 transition-colors px-1.5 py-1 flex items-center justify-center cursor-grab active:cursor-grabbing rounded-l-[3px]"
|
|
394
394
|
aria-label="Move column"
|
|
395
395
|
onMouseDown={(e) => {
|
|
396
396
|
e.stopPropagation();
|
|
@@ -404,22 +404,22 @@ export default function SectionV2Column({
|
|
|
404
404
|
<DragDropIcon size={14} />
|
|
405
405
|
<BubbleTooltip>Drag to move</BubbleTooltip>
|
|
406
406
|
</button>
|
|
407
|
-
<div className="w-px self-stretch my-1" style={{ background:
|
|
407
|
+
<div className="w-px self-stretch my-1" style={{ background: BUILDER_YELLOW }} />
|
|
408
408
|
{/* Label — clicking selects the column */}
|
|
409
409
|
<button
|
|
410
410
|
type="button"
|
|
411
411
|
onClick={(e) => { e.stopPropagation(); onSelect(); }}
|
|
412
|
-
className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#
|
|
413
|
-
style={{ color:
|
|
412
|
+
className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#d68900]/15 transition-colors"
|
|
413
|
+
style={{ color: BUILDER_YELLOW }}
|
|
414
414
|
aria-label="Select column"
|
|
415
415
|
>
|
|
416
416
|
Col
|
|
417
417
|
</button>
|
|
418
|
-
<div className="w-px self-stretch my-1" style={{ background:
|
|
418
|
+
<div className="w-px self-stretch my-1" style={{ background: BUILDER_YELLOW }} />
|
|
419
419
|
{/* Delete */}
|
|
420
420
|
<button
|
|
421
421
|
onClick={handleDelete}
|
|
422
|
-
className="group/bb relative text-[#
|
|
422
|
+
className="group/bb relative text-[#d68900] hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-1 flex items-center justify-center rounded-r-[3px]"
|
|
423
423
|
aria-label="Delete column"
|
|
424
424
|
>
|
|
425
425
|
<CloseIcon size={14} />
|
|
@@ -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}
|