@morphika/andami 0.2.26 → 0.3.1
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 +39 -45
- 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 +11 -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/ShaderCanvas.tsx +10 -6
- package/components/builder/BlockCardIcons.tsx +227 -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/SectionCardIcons.tsx +266 -0
- package/components/builder/SectionEditorBar.tsx +17 -12
- package/components/builder/SectionTypePicker.tsx +33 -137
- 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 +53 -180
- package/components/builder/iconPrimitives.tsx +78 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
- package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
- 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/assets.ts +17 -2
- package/lib/builder/constants.ts +22 -15
- package/lib/builder/format.ts +25 -0
- package/lib/builder/history.ts +0 -3
- package/lib/builder/layout-styles.ts +1 -1
- 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.ts +0 -2
- package/lib/builder/types.ts +1 -2
- package/lib/csrf.ts +31 -0
- package/lib/sanity/types.ts +4 -1
- package/lib/security.ts +50 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/objects/coverSection.ts +35 -3
- package/components/builder/ParallaxSlideHeader.tsx +0 -113
|
@@ -3,87 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { SECTION_TYPE_REGISTRY } from "../../lib/builder/types";
|
|
5
5
|
import type { CustomSectionListItem } from "../../lib/sanity/types";
|
|
6
|
-
import {
|
|
7
|
-
import { BLOCK_GRADIENTS } from "./blockStyles";
|
|
8
|
-
import { BUILDER_BLUE, BUILDER_VIOLET } from "../../lib/builder/constants";
|
|
9
|
-
|
|
10
|
-
// ── Section card icons ──
|
|
11
|
-
|
|
12
|
-
function EmptySectionV2Icon({ size = 28 }: { size?: number }) {
|
|
13
|
-
return (
|
|
14
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
15
|
-
<defs>
|
|
16
|
-
<linearGradient id="es2Grad" x1="5" y1="5" x2="35" y2="35">
|
|
17
|
-
<stop offset="0%" stopColor={BUILDER_BLUE} />
|
|
18
|
-
<stop offset="100%" stopColor="#0550c0" />
|
|
19
|
-
</linearGradient>
|
|
20
|
-
</defs>
|
|
21
|
-
{/* Grid pattern */}
|
|
22
|
-
<rect x="3" y="3" width="34" height="34" rx="4" fill="url(#es2Grad)" opacity="0.15" />
|
|
23
|
-
<rect x="3" y="3" width="34" height="34" rx="4" stroke="url(#es2Grad)" strokeWidth="2" fill="none" opacity="0.5" />
|
|
24
|
-
{/* Grid columns */}
|
|
25
|
-
<rect x="6" y="8" width="8" height="24" rx="2" fill="url(#es2Grad)" opacity="0.3" />
|
|
26
|
-
<rect x="16" y="8" width="8" height="24" rx="2" fill="url(#es2Grad)" opacity="0.3" />
|
|
27
|
-
<rect x="26" y="8" width="8" height="24" rx="2" fill="url(#es2Grad)" opacity="0.3" />
|
|
28
|
-
</svg>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function CoverSectionIcon({ size = 28 }: { size?: number }) {
|
|
33
|
-
const accent = "#0d9488";
|
|
34
|
-
return (
|
|
35
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
36
|
-
<defs>
|
|
37
|
-
<linearGradient id="csGrad" x1="5" y1="5" x2="35" y2="35">
|
|
38
|
-
<stop offset="0%" stopColor={accent} />
|
|
39
|
-
<stop offset="100%" stopColor="#0f766e" />
|
|
40
|
-
</linearGradient>
|
|
41
|
-
</defs>
|
|
42
|
-
<rect x="3" y="3" width="34" height="34" rx="4" fill="url(#csGrad)" opacity="0.15" />
|
|
43
|
-
<rect x="3" y="3" width="34" height="34" rx="4" stroke="url(#csGrad)" strokeWidth="2" fill="none" opacity="0.5" />
|
|
44
|
-
{/* Top row (large) */}
|
|
45
|
-
<rect x="6" y="6" width="28" height="18" rx="2" fill="url(#csGrad)" opacity="0.25" />
|
|
46
|
-
{/* Bottom row (small) */}
|
|
47
|
-
<rect x="6" y="26" width="28" height="8" rx="2" fill="url(#csGrad)" opacity="0.4" />
|
|
48
|
-
{/* Divider line */}
|
|
49
|
-
<line x1="8" y1="25" x2="32" y2="25" stroke={accent} strokeWidth="1" opacity="0.5" strokeDasharray="2 2" />
|
|
50
|
-
</svg>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function SavedSectionIcon({ size = 28 }: { size?: number }) {
|
|
55
|
-
return (
|
|
56
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
57
|
-
<defs>
|
|
58
|
-
<linearGradient id="savedGrad" x1="5" y1="5" x2="35" y2="35">
|
|
59
|
-
<stop offset="0%" stopColor={BUILDER_VIOLET} />
|
|
60
|
-
<stop offset="100%" stopColor="#6d28d9" />
|
|
61
|
-
</linearGradient>
|
|
62
|
-
</defs>
|
|
63
|
-
<rect x="3" y="3" width="34" height="34" rx="4" fill="url(#savedGrad)" opacity="0.15" />
|
|
64
|
-
<rect x="3" y="3" width="34" height="34" rx="4" stroke="url(#savedGrad)" strokeWidth="2" fill="none" opacity="0.5" />
|
|
65
|
-
{/* Component/puzzle icon */}
|
|
66
|
-
<rect x="8" y="8" width="10" height="10" rx="2" fill="url(#savedGrad)" opacity="0.5" />
|
|
67
|
-
<rect x="22" y="8" width="10" height="10" rx="2" fill="url(#savedGrad)" opacity="0.5" />
|
|
68
|
-
<rect x="8" y="22" width="10" height="10" rx="2" fill="url(#savedGrad)" opacity="0.5" />
|
|
69
|
-
<rect x="22" y="22" width="10" height="10" rx="2" fill="url(#savedGrad)" opacity="0.3" />
|
|
70
|
-
</svg>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const SECTION_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>> = {
|
|
75
|
-
"empty-v2": EmptySectionV2Icon,
|
|
76
|
-
coverSection: CoverSectionIcon,
|
|
77
|
-
projectGridBlock: ProjectGridBlockIcon,
|
|
78
|
-
parallaxGroup: ParallaxGroupIcon,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const SECTION_GRADIENTS: Record<string, string> = {
|
|
82
|
-
"empty-v2": "linear-gradient(135deg, #d0e0f8 0%, #b8d0f0 50%, #a0c0e8 100%)",
|
|
83
|
-
coverSection: "linear-gradient(135deg, #b2f5ea 0%, #81e6d9 50%, #5eead4 100%)",
|
|
84
|
-
projectGridBlock: BLOCK_GRADIENTS.projectGridBlock,
|
|
85
|
-
parallaxGroup: BLOCK_GRADIENTS.parallaxGroup,
|
|
86
|
-
};
|
|
6
|
+
import { SECTION_CARD_ICONS } from "./SectionCardIcons";
|
|
87
7
|
|
|
88
8
|
// ── V2 layout presets (use cascade preset names) ──
|
|
89
9
|
type V2Preset = "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3";
|
|
@@ -96,10 +16,9 @@ const layoutPresets: { key: V2Preset; label: string; widths: number[] }[] = [
|
|
|
96
16
|
{ key: "2/3+1/3", label: "2/3 + 1/3", widths: [8, 4] },
|
|
97
17
|
];
|
|
98
18
|
|
|
99
|
-
// ── Shared card
|
|
19
|
+
// ── Shared flat card (matches Add Block modal style) ──
|
|
100
20
|
|
|
101
21
|
interface SectionCardProps {
|
|
102
|
-
gradient: string;
|
|
103
22
|
icon: React.ReactNode;
|
|
104
23
|
title: string;
|
|
105
24
|
subtitle?: string;
|
|
@@ -110,7 +29,6 @@ interface SectionCardProps {
|
|
|
110
29
|
}
|
|
111
30
|
|
|
112
31
|
function SectionCard({
|
|
113
|
-
gradient,
|
|
114
32
|
icon,
|
|
115
33
|
title,
|
|
116
34
|
subtitle,
|
|
@@ -124,56 +42,39 @@ function SectionCard({
|
|
|
124
42
|
onClick={onClick}
|
|
125
43
|
onMouseEnter={onMouseEnter}
|
|
126
44
|
onMouseLeave={onMouseLeave}
|
|
127
|
-
className="relative flex items-center
|
|
45
|
+
className="relative flex items-center rounded-2xl text-left group overflow-hidden border-0 h-[96px]"
|
|
128
46
|
style={{
|
|
129
|
-
background:
|
|
130
|
-
transform: isHovered ? "translateY(-1px)
|
|
131
|
-
|
|
132
|
-
? "0 8px 24px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.3)"
|
|
133
|
-
: "0 2px 8px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.2)",
|
|
134
|
-
transition: "all 0.3s cubic-bezier(0.23, 1, 0.32, 1)",
|
|
47
|
+
background: "#f4f4f4",
|
|
48
|
+
transform: isHovered ? "translateY(-1px)" : "translateY(0)",
|
|
49
|
+
transition: "transform 200ms cubic-bezier(0.23, 1, 0.32, 1)",
|
|
135
50
|
}}
|
|
136
51
|
>
|
|
137
|
-
{/*
|
|
138
|
-
<div
|
|
139
|
-
className="absolute inset-0 rounded-2xl pointer-events-none"
|
|
140
|
-
style={{
|
|
141
|
-
background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)",
|
|
142
|
-
}}
|
|
143
|
-
/>
|
|
144
|
-
{/* Icon container */}
|
|
145
|
-
<div
|
|
146
|
-
className="relative shrink-0 flex items-center justify-center"
|
|
147
|
-
style={{
|
|
148
|
-
width: 44,
|
|
149
|
-
height: 44,
|
|
150
|
-
borderRadius: 12,
|
|
151
|
-
background: "rgba(255,255,255,0.4)",
|
|
152
|
-
backdropFilter: "blur(8px)",
|
|
153
|
-
boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
|
|
154
|
-
transition: "transform 0.3s",
|
|
155
|
-
transform: isHovered ? "scale(1.08)" : "scale(1)",
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
52
|
+
{/* Icon artwork — full-bleed on the left. */}
|
|
53
|
+
<div className="shrink-0 h-full" style={{ width: 176 }}>
|
|
158
54
|
{icon}
|
|
159
55
|
</div>
|
|
56
|
+
|
|
160
57
|
{/* Text */}
|
|
161
|
-
<div className="
|
|
162
|
-
<p
|
|
163
|
-
className="text-sm font-semibold truncate"
|
|
164
|
-
style={{ color: "rgba(0,0,0,0.72)", textShadow: "0 1px 0 rgba(255,255,255,0.3)" }}
|
|
165
|
-
>
|
|
58
|
+
<div className="min-w-0 pr-5 py-4 flex-1">
|
|
59
|
+
<p className="text-[17px] font-semibold text-[#2b2f38] truncate leading-tight">
|
|
166
60
|
{title}
|
|
167
61
|
</p>
|
|
168
62
|
{subtitle && (
|
|
169
|
-
<p
|
|
170
|
-
className="text-xs truncate leading-snug mt-0.5"
|
|
171
|
-
style={{ color: "rgba(0,0,0,0.42)" }}
|
|
172
|
-
>
|
|
63
|
+
<p className="text-[13px] text-[#9096a0] truncate leading-snug mt-1">
|
|
173
64
|
{subtitle}
|
|
174
65
|
</p>
|
|
175
66
|
)}
|
|
176
67
|
</div>
|
|
68
|
+
|
|
69
|
+
{/* Hover stroke overlay — violet accent to differentiate sections from blocks. */}
|
|
70
|
+
<span
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
className="absolute inset-0 rounded-2xl pointer-events-none"
|
|
73
|
+
style={{
|
|
74
|
+
boxShadow: isHovered ? "inset 0 0 0 2px #7500D5" : "inset 0 0 0 2px transparent",
|
|
75
|
+
transition: "box-shadow 160ms ease",
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
177
78
|
</button>
|
|
178
79
|
);
|
|
179
80
|
}
|
|
@@ -256,7 +157,7 @@ export default function SectionTypePicker({
|
|
|
256
157
|
onClick={onClose}
|
|
257
158
|
>
|
|
258
159
|
<div
|
|
259
|
-
className="w-full max-w-
|
|
160
|
+
className="w-full max-w-4xl rounded-2xl bg-white max-h-[80vh] flex flex-col shadow-2xl border border-neutral-200/50 overflow-hidden"
|
|
260
161
|
style={{ fontFamily: "Inter, system-ui, sans-serif" }}
|
|
261
162
|
onClick={(e) => e.stopPropagation()}
|
|
262
163
|
>
|
|
@@ -311,14 +212,14 @@ export default function SectionTypePicker({
|
|
|
311
212
|
}
|
|
312
213
|
onClose();
|
|
313
214
|
}}
|
|
314
|
-
className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#
|
|
215
|
+
className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#4794e2] hover:bg-[#4794e2]/5 transition-colors group shadow-sm"
|
|
315
216
|
title={label}
|
|
316
217
|
>
|
|
317
218
|
<div className="flex gap-1 h-6">
|
|
318
219
|
{widths.map((w, i) => (
|
|
319
220
|
<div
|
|
320
221
|
key={i}
|
|
321
|
-
className="bg-neutral-200 group-hover:bg-[#
|
|
222
|
+
className="bg-neutral-200 group-hover:bg-[#4794e2]/30 rounded-sm transition-colors"
|
|
322
223
|
style={{ flex: w }}
|
|
323
224
|
/>
|
|
324
225
|
))}
|
|
@@ -337,13 +238,11 @@ export default function SectionTypePicker({
|
|
|
337
238
|
</p>
|
|
338
239
|
<div className="grid grid-cols-2 gap-2.5">
|
|
339
240
|
{SECTION_TYPE_REGISTRY.map((section) => {
|
|
340
|
-
const IconComponent =
|
|
341
|
-
const gradient = SECTION_GRADIENTS[section.type] || "#f5f5f5";
|
|
241
|
+
const IconComponent = SECTION_CARD_ICONS[section.type];
|
|
342
242
|
|
|
343
243
|
return (
|
|
344
244
|
<SectionCard
|
|
345
245
|
key={section.type}
|
|
346
|
-
gradient={gradient}
|
|
347
246
|
icon={IconComponent ? <IconComponent /> : null}
|
|
348
247
|
title={section.label}
|
|
349
248
|
subtitle={section.description}
|
|
@@ -380,8 +279,7 @@ export default function SectionTypePicker({
|
|
|
380
279
|
{/* Create new section button — always visible immediately */}
|
|
381
280
|
{onCreateCustomSection && (
|
|
382
281
|
<SectionCard
|
|
383
|
-
|
|
384
|
-
icon={<span className="text-[#8b5cf6] text-xl font-light">+</span>}
|
|
282
|
+
icon={(() => { const I = SECTION_CARD_ICONS.createCustom; return I ? <I /> : null; })()}
|
|
385
283
|
title="Create New"
|
|
386
284
|
subtitle="Build a reusable section"
|
|
387
285
|
isHovered={hovered === "create-custom"}
|
|
@@ -400,13 +298,12 @@ export default function SectionTypePicker({
|
|
|
400
298
|
{[1, 2].map((i) => (
|
|
401
299
|
<div
|
|
402
300
|
key={`skeleton-${i}`}
|
|
403
|
-
className="flex items-center
|
|
404
|
-
style={{ background: "linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%)" }}
|
|
301
|
+
className="flex items-center rounded-2xl h-[96px] bg-[#f4f4f4] animate-pulse overflow-hidden"
|
|
405
302
|
>
|
|
406
|
-
<div className="w-
|
|
407
|
-
<div className="flex-1 space-y-
|
|
408
|
-
<div className="h-
|
|
409
|
-
<div className="h-
|
|
303
|
+
<div className="w-[176px] h-full bg-white/40" />
|
|
304
|
+
<div className="flex-1 pr-5 py-4 space-y-2">
|
|
305
|
+
<div className="h-4 bg-white/60 rounded w-28" />
|
|
306
|
+
<div className="h-3 bg-white/50 rounded w-20" />
|
|
410
307
|
</div>
|
|
411
308
|
</div>
|
|
412
309
|
))}
|
|
@@ -424,8 +321,7 @@ export default function SectionTypePicker({
|
|
|
424
321
|
{!loadingSaved && savedSections.map((section) => (
|
|
425
322
|
<SectionCard
|
|
426
323
|
key={section._id}
|
|
427
|
-
|
|
428
|
-
icon={<SavedSectionIcon size={20} />}
|
|
324
|
+
icon={(() => { const I = SECTION_CARD_ICONS.savedCustom; return I ? <I /> : null; })()}
|
|
429
325
|
title={section.title}
|
|
430
326
|
subtitle={section.description}
|
|
431
327
|
isHovered={hovered === section._id}
|
|
@@ -285,7 +285,7 @@ export default function SectionV2Canvas({
|
|
|
285
285
|
: showAsDropTarget
|
|
286
286
|
? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
|
|
287
287
|
: isSectionHovered
|
|
288
|
-
? "border-[#
|
|
288
|
+
? "border-[#4794e2]/25 text-[#4794e2]/50 hover:text-[#4794e2] hover:border-[#4794e2]/60 hover:bg-[#4794e2]/5 opacity-100"
|
|
289
289
|
: "border-transparent text-transparent opacity-0 pointer-events-none"
|
|
290
290
|
}`}
|
|
291
291
|
>
|
|
@@ -84,17 +84,17 @@ function ResizeHandle({
|
|
|
84
84
|
height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
|
|
85
85
|
borderRadius: isActive ? "50%" : 999,
|
|
86
86
|
backgroundColor: isActive
|
|
87
|
-
? "rgba(
|
|
87
|
+
? "rgba(71, 148, 226, 0.9)"
|
|
88
88
|
: isHoveredEdge
|
|
89
|
-
? "rgba(
|
|
89
|
+
? "rgba(71, 148, 226, 0.7)"
|
|
90
90
|
: showChrome
|
|
91
|
-
? "rgba(
|
|
92
|
-
: "rgba(
|
|
91
|
+
? "rgba(71, 148, 226, 0.5)"
|
|
92
|
+
: "rgba(71, 148, 226, 0.2)",
|
|
93
93
|
transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
|
|
94
94
|
boxShadow: isActive
|
|
95
|
-
? "0 0 10px rgba(
|
|
95
|
+
? "0 0 10px rgba(71, 148, 226, 0.5)"
|
|
96
96
|
: isHoveredEdge
|
|
97
|
-
? "0 0 6px rgba(
|
|
97
|
+
? "0 0 6px rgba(71, 148, 226, 0.2)"
|
|
98
98
|
: undefined,
|
|
99
99
|
}}
|
|
100
100
|
/>
|
|
@@ -287,15 +287,15 @@ export default function SectionV2Column({
|
|
|
287
287
|
style={{
|
|
288
288
|
transition: "box-shadow 150ms, border 150ms",
|
|
289
289
|
...(isSwapTarget
|
|
290
|
-
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(
|
|
290
|
+
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(71, 148, 226, 0.08)" }
|
|
291
291
|
: isBlockOver
|
|
292
292
|
? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
|
|
293
293
|
: isSelected
|
|
294
|
-
? { boxShadow: `inset 0 0 0 2px rgba(
|
|
294
|
+
? { boxShadow: `inset 0 0 0 2px rgba(71, 148, 226, 0.6)` }
|
|
295
295
|
: isHovered
|
|
296
|
-
? { boxShadow: `inset 0 0 0 1.5px rgba(
|
|
296
|
+
? { boxShadow: `inset 0 0 0 1.5px rgba(71, 148, 226, 0.5)` }
|
|
297
297
|
: showFaintOutline
|
|
298
|
-
? { border: `1px dashed rgba(
|
|
298
|
+
? { border: `1px dashed rgba(71, 148, 226, 0.2)`, borderRadius: 4 }
|
|
299
299
|
: undefined),
|
|
300
300
|
}}
|
|
301
301
|
/>
|
|
@@ -350,18 +350,6 @@ export default function SectionV2Column({
|
|
|
350
350
|
</>
|
|
351
351
|
)}
|
|
352
352
|
|
|
353
|
-
{/* Span badge — top right */}
|
|
354
|
-
<div
|
|
355
|
-
className={`absolute top-0 right-0 z-[5] transition-opacity ${
|
|
356
|
-
isSelected || isHovered ? "opacity-100" : showFaintOutline ? "opacity-40" : "opacity-0"
|
|
357
|
-
}`}
|
|
358
|
-
style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "top right" }}
|
|
359
|
-
>
|
|
360
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded-bl rounded-tr bg-[#076bff]/80 text-white/90 font-medium tabular-nums">
|
|
361
|
-
{column.span}/{gridColumns}
|
|
362
|
-
</span>
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
353
|
{/* Delete button — red circle top right, positioned outside the column box.
|
|
366
354
|
Nested pattern: outer div positions, inner div counter-scales. */}
|
|
367
355
|
<div
|
|
@@ -378,7 +366,8 @@ export default function SectionV2Column({
|
|
|
378
366
|
<div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
|
|
379
367
|
<button
|
|
380
368
|
onClick={handleDelete}
|
|
381
|
-
className="w-5 h-5 rounded-full
|
|
369
|
+
className="w-5 h-5 rounded-full text-white flex items-center justify-center transition-transform hover:scale-[1.15]"
|
|
370
|
+
style={{ background: "#ef4848" }}
|
|
382
371
|
title="Delete column"
|
|
383
372
|
aria-label="Delete column"
|
|
384
373
|
>
|
|
@@ -403,7 +392,7 @@ export default function SectionV2Column({
|
|
|
403
392
|
>
|
|
404
393
|
<div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
|
|
405
394
|
<div
|
|
406
|
-
className="w-5 h-5 rounded-full bg-[#
|
|
395
|
+
className="w-5 h-5 rounded-full bg-[#4794e2] text-white flex items-center justify-center shadow-md cursor-grab active:cursor-grabbing transition-transform hover:scale-[1.15] hover:bg-[#3578b8] hover:shadow-blue-500/30 hover:shadow-lg"
|
|
407
396
|
title="Drag to move column"
|
|
408
397
|
aria-label="Move column"
|
|
409
398
|
onMouseDown={(e) => {
|
|
@@ -479,9 +468,9 @@ export default function SectionV2Column({
|
|
|
479
468
|
style={{
|
|
480
469
|
padding: "5px 16px",
|
|
481
470
|
pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
|
|
482
|
-
background:
|
|
483
|
-
color: "#
|
|
484
|
-
border:
|
|
471
|
+
background: "#d2e3ff",
|
|
472
|
+
color: "#4794e2",
|
|
473
|
+
border: "1px dashed #4794e2",
|
|
485
474
|
}}
|
|
486
475
|
>
|
|
487
476
|
+ Add Block
|
|
@@ -508,9 +497,9 @@ export default function SectionV2Column({
|
|
|
508
497
|
style={{
|
|
509
498
|
padding: "4px 14px",
|
|
510
499
|
pointerEvents: showChrome ? "auto" : "none",
|
|
511
|
-
background: "
|
|
512
|
-
color: "#
|
|
513
|
-
border: "1px dashed
|
|
500
|
+
background: "#d2e3ff",
|
|
501
|
+
color: "#4794e2",
|
|
502
|
+
border: "1px dashed #4794e2",
|
|
514
503
|
}}
|
|
515
504
|
>
|
|
516
505
|
+ Add Block
|
|
@@ -58,7 +58,6 @@ export default function SettingsPanel() {
|
|
|
58
58
|
selectedColumnV2,
|
|
59
59
|
selectedBlock,
|
|
60
60
|
panelTitle,
|
|
61
|
-
headerGradient,
|
|
62
61
|
HeaderIconComponent,
|
|
63
62
|
isColumnOnly,
|
|
64
63
|
isParallaxGroupOnly,
|
|
@@ -93,44 +92,21 @@ export default function SettingsPanel() {
|
|
|
93
92
|
|
|
94
93
|
return (
|
|
95
94
|
<div className="w-72 border-l border-[#f0f0f0] bg-white overflow-y-auto shrink-0 flex flex-col">
|
|
96
|
-
{/* Panel header —
|
|
97
|
-
<div
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<div
|
|
103
|
-
className="absolute inset-0 pointer-events-none"
|
|
104
|
-
style={{
|
|
105
|
-
background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)",
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
|
|
109
|
-
{/* Icon container — frosted glass */}
|
|
110
|
-
<div
|
|
111
|
-
className="relative shrink-0 flex items-center justify-center"
|
|
112
|
-
style={{
|
|
113
|
-
width: 36,
|
|
114
|
-
height: 36,
|
|
115
|
-
borderRadius: 10,
|
|
116
|
-
background: "rgba(255,255,255,0.4)",
|
|
117
|
-
backdropFilter: "blur(8px)",
|
|
118
|
-
boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
{HeaderIconComponent ? <HeaderIconComponent size={22} /> : null}
|
|
95
|
+
{/* Panel header — flat gray, matching the new Add Block card style */}
|
|
96
|
+
<div className="relative flex items-center px-3.5 py-3 shrink-0 bg-[#f4f4f4]">
|
|
97
|
+
{/* Icon — landscape aspect (card icon at smaller scale). Bumped to
|
|
98
|
+
size 40 now that the new icons carry less construction detail. */}
|
|
99
|
+
<div className="shrink-0 flex items-center" style={{ height: 44 }}>
|
|
100
|
+
{HeaderIconComponent ? <HeaderIconComponent size={40} /> : null}
|
|
122
101
|
</div>
|
|
123
102
|
|
|
124
103
|
{/* Title */}
|
|
125
|
-
<h3
|
|
126
|
-
className="relative z-10 ml-2.5 text-[13px] font-semibold truncate"
|
|
127
|
-
style={{ color: "rgba(0,0,0,0.72)", textShadow: "0 1px 0 rgba(255,255,255,0.3)" }}
|
|
128
|
-
>
|
|
104
|
+
<h3 className="ml-2.5 text-[13px] font-semibold text-[#2b2f38] truncate">
|
|
129
105
|
{panelTitle}
|
|
130
106
|
</h3>
|
|
131
107
|
|
|
132
108
|
{/* Action button — single delete for the active selection (block > column > section priority) */}
|
|
133
|
-
<div className="
|
|
109
|
+
<div className="flex items-center gap-0.5 ml-auto">
|
|
134
110
|
{(() => {
|
|
135
111
|
// Determine the single delete action based on selection priority: block > column > section
|
|
136
112
|
let onDelete: (() => void) | null = null;
|
|
@@ -11,7 +11,7 @@ import type { ContentBlock, ImageBlock, VideoBlock } from "../../lib/sanity/type
|
|
|
11
11
|
import BlockLivePreview from "./BlockLivePreview";
|
|
12
12
|
import { getBlockAlignmentStyles, hasBlockAlignment } from "../../lib/builder/layout-styles";
|
|
13
13
|
import type { BlockLayout } from "../../lib/sanity/types";
|
|
14
|
-
import {
|
|
14
|
+
import { BUILDER_BLOCK } from "../../lib/builder/constants";
|
|
15
15
|
|
|
16
16
|
interface SortableBlockProps {
|
|
17
17
|
block: ContentBlock;
|
|
@@ -124,7 +124,7 @@ export default function SortableBlock({
|
|
|
124
124
|
style={{ ...style, ...(!isFillBlock ? { position: "relative" as const, zIndex: 1 } : {}) }}
|
|
125
125
|
className={`transition-[opacity,box-shadow] ${
|
|
126
126
|
isDragging
|
|
127
|
-
? "ring-2 ring-[#
|
|
127
|
+
? "ring-2 ring-[#4794e2] ring-offset-1 ring-offset-transparent rounded"
|
|
128
128
|
: ""
|
|
129
129
|
}`}
|
|
130
130
|
onClick={(e) => {
|
|
@@ -143,9 +143,9 @@ export default function SortableBlock({
|
|
|
143
143
|
style={{
|
|
144
144
|
inset: `${Math.max(2, Math.min(5, 3 / canvasZoom))}px`,
|
|
145
145
|
...(isSelected
|
|
146
|
-
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${
|
|
146
|
+
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${BUILDER_BLOCK}` }
|
|
147
147
|
: isHovered
|
|
148
|
-
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(
|
|
148
|
+
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(71, 148, 226, 0.4)` }
|
|
149
149
|
: {}),
|
|
150
150
|
}}
|
|
151
151
|
/>
|
|
@@ -166,17 +166,37 @@ export default function SortableBlock({
|
|
|
166
166
|
style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "top center" }}
|
|
167
167
|
>
|
|
168
168
|
<div className="flex items-center rounded-[5px] overflow-hidden" style={{
|
|
169
|
-
background: "
|
|
170
|
-
|
|
171
|
-
border: "1px solid rgba(255,255,255,0.06)",
|
|
169
|
+
background: "#d2e3ff",
|
|
170
|
+
border: "1px solid #4794e2",
|
|
172
171
|
}}>
|
|
172
|
+
{/* Block type label — first */}
|
|
173
|
+
<span className="text-[11px] px-1.5 py-0.5 font-medium" style={{ color: "#4794e2" }}>
|
|
174
|
+
{info?.icon || "▪"} {info?.label || block._type}
|
|
175
|
+
</span>
|
|
176
|
+
{/* Enter animation badge */}
|
|
177
|
+
{block.enter_animation?.preset && block.enter_animation.preset !== "none" && (
|
|
178
|
+
<span className="text-[10px] text-[#4794e2]/50 px-1 py-0.5 border-l border-[#4794e2]/25" title={`Animation: ${block.enter_animation.preset}`}>
|
|
179
|
+
✦
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
{/* Duplicate */}
|
|
183
|
+
{onDuplicate && (
|
|
184
|
+
<button
|
|
185
|
+
onClick={onDuplicate}
|
|
186
|
+
className="text-[#4794e2]/60 hover:text-[#4794e2] transition-colors px-1.5 py-0.5 text-[11px] border-l border-[#4794e2]/25 hover:bg-[#4794e2]/10"
|
|
187
|
+
title="Duplicate block (Ctrl+D)"
|
|
188
|
+
aria-label="Duplicate block"
|
|
189
|
+
>
|
|
190
|
+
⧉
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
173
193
|
{/* Move up arrow */}
|
|
174
194
|
<button
|
|
175
195
|
onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
|
|
176
|
-
className={`transition-colors px-1 py-0.5 text-[11px] ${
|
|
196
|
+
className={`transition-colors px-1 py-0.5 text-[11px] border-l border-[#4794e2]/25 ${
|
|
177
197
|
canMoveUp
|
|
178
|
-
? "text-
|
|
179
|
-
: "text-
|
|
198
|
+
? "text-[#4794e2]/60 hover:text-[#4794e2] hover:bg-[#4794e2]/10 cursor-pointer"
|
|
199
|
+
: "text-[#4794e2]/25 cursor-default"
|
|
180
200
|
}`}
|
|
181
201
|
title="Move block up"
|
|
182
202
|
aria-label="Move block up"
|
|
@@ -189,10 +209,10 @@ export default function SortableBlock({
|
|
|
189
209
|
{/* Move down arrow */}
|
|
190
210
|
<button
|
|
191
211
|
onClick={() => canMoveDown && reorderBlocks(rowKey, colKey, blockIndex, blockIndex + 1)}
|
|
192
|
-
className={`transition-colors px-1 py-0.5 text-[11px] border-l border-
|
|
212
|
+
className={`transition-colors px-1 py-0.5 text-[11px] border-l border-[#4794e2]/25 ${
|
|
193
213
|
canMoveDown
|
|
194
|
-
? "text-
|
|
195
|
-
: "text-
|
|
214
|
+
? "text-[#4794e2]/60 hover:text-[#4794e2] hover:bg-[#4794e2]/10 cursor-pointer"
|
|
215
|
+
: "text-[#4794e2]/25 cursor-default"
|
|
196
216
|
}`}
|
|
197
217
|
title="Move block down"
|
|
198
218
|
aria-label="Move block down"
|
|
@@ -202,44 +222,16 @@ export default function SortableBlock({
|
|
|
202
222
|
<path d="M5 8L2 4h6L5 8z" fill="currentColor" />
|
|
203
223
|
</svg>
|
|
204
224
|
</button>
|
|
205
|
-
{/*
|
|
206
|
-
<
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)}
|
|
215
|
-
{/* Duplicate */}
|
|
216
|
-
{onDuplicate && (
|
|
217
|
-
<button
|
|
218
|
-
onClick={onDuplicate}
|
|
219
|
-
className="text-white/45 hover:text-white/80 transition-colors px-1.5 py-0.5 text-[11px] border-l border-white/10 hover:bg-white/10"
|
|
220
|
-
title="Duplicate block (Ctrl+D)"
|
|
221
|
-
aria-label="Duplicate block"
|
|
222
|
-
>
|
|
223
|
-
⧉
|
|
224
|
-
</button>
|
|
225
|
-
)}
|
|
225
|
+
{/* Delete — text inside pill, red hover for destructive signal */}
|
|
226
|
+
<button
|
|
227
|
+
onClick={onDelete}
|
|
228
|
+
className="text-[#4794e2]/60 hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-0.5 text-[11px] font-medium border-l border-[#4794e2]/25"
|
|
229
|
+
title="Delete block"
|
|
230
|
+
aria-label="Delete block"
|
|
231
|
+
>
|
|
232
|
+
Delete
|
|
233
|
+
</button>
|
|
226
234
|
</div>
|
|
227
|
-
{/* Delete button — inline at end of toolbar with gap separator */}
|
|
228
|
-
<button
|
|
229
|
-
onClick={onDelete}
|
|
230
|
-
className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-md shrink-0"
|
|
231
|
-
title="Delete block"
|
|
232
|
-
aria-label="Delete block"
|
|
233
|
-
>
|
|
234
|
-
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
235
|
-
<path
|
|
236
|
-
d="M2 2l6 6M8 2l-6 6"
|
|
237
|
-
stroke="currentColor"
|
|
238
|
-
strokeWidth="1.5"
|
|
239
|
-
strokeLinecap="round"
|
|
240
|
-
/>
|
|
241
|
-
</svg>
|
|
242
|
-
</button>
|
|
243
235
|
</div>
|
|
244
236
|
</div>
|
|
245
237
|
|