@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
|
@@ -8,11 +8,12 @@ import { useBuilderStore } from "../../lib/builder/store";
|
|
|
8
8
|
import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
|
|
9
9
|
import { DEVICE_HEIGHTS, isSectionBlockSection } from "../../lib/builder/types";
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
|
-
import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
|
|
12
|
-
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
11
|
+
import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup, CoverSection } from "../../lib/sanity/types";
|
|
12
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
13
13
|
import { getRowLayoutStyles } from "../../lib/builder/layout-styles";
|
|
14
14
|
import { normalizeMinHeight } from "../../lib/builder/utils";
|
|
15
15
|
import { getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
|
|
16
|
+
import { formatRowPercent } from "../../lib/builder/format";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Convert vh-based CSS values to pixels using the simulated device viewport height.
|
|
@@ -40,6 +41,9 @@ function getSectionLabel(item: ContentItem): string | null {
|
|
|
40
41
|
if (isParallaxGroup(item)) {
|
|
41
42
|
return "Parallax Showcase";
|
|
42
43
|
}
|
|
44
|
+
if (isCoverSection(item)) {
|
|
45
|
+
return "Cover Section";
|
|
46
|
+
}
|
|
43
47
|
if (isPageSectionV2(item)) {
|
|
44
48
|
const section = item as PageSectionV2;
|
|
45
49
|
if (isSectionBlockSection(section)) {
|
|
@@ -52,6 +56,7 @@ function getSectionLabel(item: ContentItem): string | null {
|
|
|
52
56
|
return null;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
|
|
55
60
|
interface SortableRowProps {
|
|
56
61
|
rowKey: string;
|
|
57
62
|
row: ContentItem;
|
|
@@ -90,6 +95,13 @@ export default function SortableRow({
|
|
|
90
95
|
const canvasZoom = useBuilderStore((s) => s.canvasZoom);
|
|
91
96
|
const activeViewport = useBuilderStore((s) => s.activeViewport);
|
|
92
97
|
const customSectionCache = useBuilderStore((s) => s._customSectionCache);
|
|
98
|
+
const addCoverRow = useBuilderStore((s) => s.addCoverRow);
|
|
99
|
+
const removeCoverRow = useBuilderStore((s) => s.removeCoverRow);
|
|
100
|
+
const addParallaxSlide = useBuilderStore((s) => s.addParallaxSlide);
|
|
101
|
+
const removeParallaxSlide = useBuilderStore((s) => s.removeParallaxSlide);
|
|
102
|
+
const moveParallaxSlide = useBuilderStore((s) => s.moveParallaxSlide);
|
|
103
|
+
const selectRow = useBuilderStore((s) => s.selectRow);
|
|
104
|
+
const selectedRowKey = useBuilderStore((s) => s.selectedRowKey);
|
|
93
105
|
const [isHovered, setIsHovered] = useState(false);
|
|
94
106
|
const {
|
|
95
107
|
attributes,
|
|
@@ -236,20 +248,20 @@ export default function SortableRow({
|
|
|
236
248
|
style={{
|
|
237
249
|
inset: `${-Math.max(2, Math.min(5, 3 / canvasZoom))}px`,
|
|
238
250
|
...(isSelected
|
|
239
|
-
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px #
|
|
251
|
+
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px #7500d5` }
|
|
240
252
|
: isHovered
|
|
241
|
-
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(
|
|
253
|
+
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(117, 0, 213, 0.4)` }
|
|
242
254
|
: {}),
|
|
243
255
|
}}
|
|
244
256
|
/>
|
|
245
257
|
|
|
246
|
-
{/* Section toolbar —
|
|
258
|
+
{/* Section toolbar — floating pill aligned top-left outside the row (8px gap) */}
|
|
247
259
|
<div
|
|
248
260
|
className={`absolute top-0 left-0 z-[5] flex flex-col items-stretch transition-opacity ${
|
|
249
261
|
showToolbar ? "opacity-100" : "opacity-0 pointer-events-none"
|
|
250
262
|
}`}
|
|
251
263
|
style={{
|
|
252
|
-
transform: `translateX(-100%) scale(${Math.min(1.5, 1 / canvasZoom)})`,
|
|
264
|
+
transform: `translateX(calc(-100% - 8px)) scale(${Math.min(1.5, 1 / canvasZoom)})`,
|
|
253
265
|
transformOrigin: "top right",
|
|
254
266
|
width: "90px",
|
|
255
267
|
}}
|
|
@@ -264,18 +276,16 @@ export default function SortableRow({
|
|
|
264
276
|
>
|
|
265
277
|
{/* Main toolbar — drag + actions */}
|
|
266
278
|
<div
|
|
267
|
-
className="flex flex-col items-stretch rounded-
|
|
279
|
+
className="flex flex-col items-stretch rounded-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
|
|
268
280
|
style={{
|
|
269
|
-
background: "
|
|
270
|
-
|
|
271
|
-
border: "1px solid rgba(255,255,255,0.06)",
|
|
272
|
-
borderRight: "none",
|
|
281
|
+
background: "#e0daff",
|
|
282
|
+
border: "1px solid #7500d5",
|
|
273
283
|
}}
|
|
274
284
|
{...attributes}
|
|
275
285
|
{...listeners}
|
|
276
286
|
>
|
|
277
287
|
{/* Section label — shows specific type for page sections */}
|
|
278
|
-
<span className="text-[11px] select-none leading-tight pointer-events-none font-medium tracking-wide" style={{ color: "
|
|
288
|
+
<span className="text-[11px] select-none leading-tight pointer-events-none font-medium tracking-wide" style={{ color: "#7500d5" }}>
|
|
279
289
|
{sectionLabel || "Section"}
|
|
280
290
|
</span>
|
|
281
291
|
|
|
@@ -284,7 +294,10 @@ export default function SortableRow({
|
|
|
284
294
|
<button
|
|
285
295
|
onClick={(e) => { e.stopPropagation(); onDuplicate(); }}
|
|
286
296
|
onPointerDown={(e) => e.stopPropagation()}
|
|
287
|
-
className="flex items-center justify-center text-[12px]
|
|
297
|
+
className="flex items-center justify-center text-[12px] transition-colors"
|
|
298
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
299
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
300
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
288
301
|
title="Duplicate section"
|
|
289
302
|
aria-label="Duplicate section"
|
|
290
303
|
>
|
|
@@ -294,7 +307,10 @@ export default function SortableRow({
|
|
|
294
307
|
onClick={(e) => { e.stopPropagation(); onMoveUp(); }}
|
|
295
308
|
onPointerDown={(e) => e.stopPropagation()}
|
|
296
309
|
disabled={isFirst}
|
|
297
|
-
className="flex items-center justify-center text-[12px]
|
|
310
|
+
className="flex items-center justify-center text-[12px] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
311
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
312
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
313
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
298
314
|
title="Move up"
|
|
299
315
|
aria-label="Move section up"
|
|
300
316
|
>
|
|
@@ -304,7 +320,10 @@ export default function SortableRow({
|
|
|
304
320
|
onClick={(e) => { e.stopPropagation(); onMoveDown(); }}
|
|
305
321
|
onPointerDown={(e) => e.stopPropagation()}
|
|
306
322
|
disabled={isLast}
|
|
307
|
-
className="flex items-center justify-center text-[12px]
|
|
323
|
+
className="flex items-center justify-center text-[12px] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
324
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
325
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
326
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
308
327
|
title="Move down"
|
|
309
328
|
aria-label="Move section down"
|
|
310
329
|
>
|
|
@@ -317,11 +336,14 @@ export default function SortableRow({
|
|
|
317
336
|
<button
|
|
318
337
|
onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
|
|
319
338
|
onPointerDown={(e) => e.stopPropagation()}
|
|
320
|
-
className="flex items-center gap-1 text-[11px]
|
|
339
|
+
className="flex items-center gap-1 text-[11px] transition-colors py-0.5"
|
|
340
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
341
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
342
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
321
343
|
title={`Add ${addColumnLabel.toLowerCase()}`}
|
|
322
344
|
aria-label={`Add ${addColumnLabel.toLowerCase()}`}
|
|
323
345
|
>
|
|
324
|
-
<span
|
|
346
|
+
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> {addColumnLabel}
|
|
325
347
|
</button>
|
|
326
348
|
)}
|
|
327
349
|
|
|
@@ -329,13 +351,179 @@ export default function SortableRow({
|
|
|
329
351
|
<button
|
|
330
352
|
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
331
353
|
onPointerDown={(e) => e.stopPropagation()}
|
|
332
|
-
className="flex items-center gap-1 text-[11px]
|
|
354
|
+
className="flex items-center gap-1 text-[11px] transition-colors py-0.5"
|
|
355
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
356
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
357
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
333
358
|
title="Delete section"
|
|
334
359
|
aria-label="Delete section"
|
|
335
360
|
>
|
|
336
|
-
<span
|
|
361
|
+
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>-</span> Delete
|
|
337
362
|
</button>
|
|
338
363
|
</div>
|
|
364
|
+
|
|
365
|
+
{/* Cover rows pill — replaces the former top "Cover Section" banner.
|
|
366
|
+
Lists each row with its height percent + remove button, plus a
|
|
367
|
+
"+ Row" action at the bottom. Only rendered for Cover sections. */}
|
|
368
|
+
{isCoverSection(row) && (() => {
|
|
369
|
+
const coverSection = row as CoverSection;
|
|
370
|
+
const coverRows = coverSection.cover_rows || [];
|
|
371
|
+
const canAddRow = coverRows.length < 5;
|
|
372
|
+
const canRemoveRow = coverRows.length > 1;
|
|
373
|
+
return (
|
|
374
|
+
<div
|
|
375
|
+
className="flex flex-col items-stretch rounded-lg py-1.5 px-2 mt-2 gap-0.5"
|
|
376
|
+
style={{
|
|
377
|
+
background: "#e0daff",
|
|
378
|
+
border: "1px solid #7500d5",
|
|
379
|
+
}}
|
|
380
|
+
onClick={(e) => e.stopPropagation()}
|
|
381
|
+
>
|
|
382
|
+
{coverRows.map((r) => (
|
|
383
|
+
<div key={r._key} className="flex items-center justify-between gap-1 py-0.5">
|
|
384
|
+
<span className="text-[11px] font-medium" style={{ color: "#7500d5" }}>
|
|
385
|
+
{formatRowPercent(r.height_percent)}% Row
|
|
386
|
+
</span>
|
|
387
|
+
<button
|
|
388
|
+
onClick={(e) => {
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
if (canRemoveRow) removeCoverRow(coverSection._key, r._key);
|
|
391
|
+
}}
|
|
392
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
393
|
+
disabled={!canRemoveRow}
|
|
394
|
+
className="flex items-center justify-center text-[12px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
395
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
396
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
397
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
398
|
+
title={canRemoveRow ? "Remove row" : "Cover must have at least 1 row"}
|
|
399
|
+
aria-label="Remove row"
|
|
400
|
+
>
|
|
401
|
+
×
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
|
|
406
|
+
{/* + Row */}
|
|
407
|
+
<button
|
|
408
|
+
onClick={(e) => {
|
|
409
|
+
e.stopPropagation();
|
|
410
|
+
if (canAddRow) addCoverRow(coverSection._key);
|
|
411
|
+
}}
|
|
412
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
413
|
+
disabled={!canAddRow}
|
|
414
|
+
className="flex items-center gap-1 text-[11px] transition-colors py-0.5 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
415
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
416
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
417
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
418
|
+
title={canAddRow ? "Add row" : "Cover supports up to 5 rows"}
|
|
419
|
+
aria-label="Add row"
|
|
420
|
+
>
|
|
421
|
+
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> Row
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
})()}
|
|
426
|
+
|
|
427
|
+
{/* Parallax slides pill — replaces the former group banner + per-slide
|
|
428
|
+
headers + bottom "+ Add Slide" button. Lists each slide with
|
|
429
|
+
click-to-select, reorder arrows, delete, and a "+ Slide" action. */}
|
|
430
|
+
{isParallaxGroup(row) && (() => {
|
|
431
|
+
const parallaxGroup = row as ParallaxGroup;
|
|
432
|
+
const slides = parallaxGroup.slides || [];
|
|
433
|
+
const canRemoveSlide = slides.length > 1;
|
|
434
|
+
return (
|
|
435
|
+
<div
|
|
436
|
+
className="flex flex-col items-stretch rounded-lg py-1.5 px-2 mt-2 gap-0.5"
|
|
437
|
+
style={{
|
|
438
|
+
background: "#e0daff",
|
|
439
|
+
border: "1px solid #7500d5",
|
|
440
|
+
}}
|
|
441
|
+
onClick={(e) => e.stopPropagation()}
|
|
442
|
+
>
|
|
443
|
+
{slides.map((slide, idx) => {
|
|
444
|
+
const isActive = selectedRowKey === slide._key;
|
|
445
|
+
return (
|
|
446
|
+
<div
|
|
447
|
+
key={slide._key}
|
|
448
|
+
onClick={(e) => { e.stopPropagation(); selectRow(slide._key); }}
|
|
449
|
+
className="flex items-center justify-between gap-1 py-0.5 px-1 -mx-1 rounded cursor-pointer transition-colors"
|
|
450
|
+
style={{ background: isActive ? "rgba(117, 0, 213, 0.15)" : "transparent" }}
|
|
451
|
+
onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = "rgba(117, 0, 213, 0.08)"; }}
|
|
452
|
+
onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.background = "transparent"; }}
|
|
453
|
+
>
|
|
454
|
+
<span className="text-[11px] font-medium" style={{ color: "#7500d5" }}>
|
|
455
|
+
Slide {idx + 1}
|
|
456
|
+
</span>
|
|
457
|
+
<div className="flex items-center gap-0.5">
|
|
458
|
+
<button
|
|
459
|
+
onClick={(e) => {
|
|
460
|
+
e.stopPropagation();
|
|
461
|
+
if (idx > 0) moveParallaxSlide(parallaxGroup._key, slide._key, "up");
|
|
462
|
+
}}
|
|
463
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
464
|
+
disabled={idx === 0}
|
|
465
|
+
className="flex items-center justify-center text-[10px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
466
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
467
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
468
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
469
|
+
title="Move slide up"
|
|
470
|
+
aria-label="Move slide up"
|
|
471
|
+
>
|
|
472
|
+
↑
|
|
473
|
+
</button>
|
|
474
|
+
<button
|
|
475
|
+
onClick={(e) => {
|
|
476
|
+
e.stopPropagation();
|
|
477
|
+
if (idx < slides.length - 1) moveParallaxSlide(parallaxGroup._key, slide._key, "down");
|
|
478
|
+
}}
|
|
479
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
480
|
+
disabled={idx === slides.length - 1}
|
|
481
|
+
className="flex items-center justify-center text-[10px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
482
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
483
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
484
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
485
|
+
title="Move slide down"
|
|
486
|
+
aria-label="Move slide down"
|
|
487
|
+
>
|
|
488
|
+
↓
|
|
489
|
+
</button>
|
|
490
|
+
<button
|
|
491
|
+
onClick={(e) => {
|
|
492
|
+
e.stopPropagation();
|
|
493
|
+
if (canRemoveSlide) removeParallaxSlide(parallaxGroup._key, slide._key);
|
|
494
|
+
}}
|
|
495
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
496
|
+
disabled={!canRemoveSlide}
|
|
497
|
+
className="flex items-center justify-center text-[12px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
498
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
499
|
+
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
500
|
+
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
501
|
+
title={canRemoveSlide ? "Remove slide" : "Parallax must have at least 1 slide"}
|
|
502
|
+
aria-label="Remove slide"
|
|
503
|
+
>
|
|
504
|
+
×
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
})}
|
|
510
|
+
|
|
511
|
+
{/* + Slide */}
|
|
512
|
+
<button
|
|
513
|
+
onClick={(e) => { e.stopPropagation(); addParallaxSlide(parallaxGroup._key); }}
|
|
514
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
515
|
+
className="flex items-center gap-1 text-[11px] transition-colors py-0.5 mt-0.5"
|
|
516
|
+
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
517
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
518
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
519
|
+
title="Add slide"
|
|
520
|
+
aria-label="Add slide"
|
|
521
|
+
>
|
|
522
|
+
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> Slide
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
})()}
|
|
339
527
|
</div>
|
|
340
528
|
|
|
341
529
|
{/* Row bg color indicator */}
|
|
@@ -3,8 +3,32 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Shared block visual styles — gradients and SVG icon components.
|
|
5
5
|
* Used by BlockTypePicker (add block cards) and SettingsPanel (header).
|
|
6
|
+
*
|
|
7
|
+
* The compact block/section icons exported here are thin wrappers that render
|
|
8
|
+
* the full card icons (`BlockCardIcons.tsx` / `SectionCardIcons.tsx`) at a
|
|
9
|
+
* smaller size. This keeps iconography 100% consistent between the modal
|
|
10
|
+
* cards and the settings panel header — same visual, just scaled.
|
|
11
|
+
*
|
|
12
|
+
* `size` represents the HEIGHT in pixels. Width is derived from the
|
|
13
|
+
* card icon's 220×120 aspect ratio (≈ height × 1.833).
|
|
6
14
|
*/
|
|
7
15
|
|
|
16
|
+
import {
|
|
17
|
+
TextBlockCardIcon,
|
|
18
|
+
ImageBlockCardIcon,
|
|
19
|
+
ImageGridBlockCardIcon,
|
|
20
|
+
VideoBlockCardIcon,
|
|
21
|
+
SpacerBlockCardIcon,
|
|
22
|
+
ButtonBlockCardIcon,
|
|
23
|
+
} from "./BlockCardIcons";
|
|
24
|
+
import {
|
|
25
|
+
CoverSectionCardIcon,
|
|
26
|
+
EmptySectionV2CardIcon,
|
|
27
|
+
ParallaxGroupCardIcon,
|
|
28
|
+
ProjectGridCardIcon,
|
|
29
|
+
SavedSectionCardIcon,
|
|
30
|
+
} from "./SectionCardIcons";
|
|
31
|
+
|
|
8
32
|
// ── Gradient backgrounds per block type ──
|
|
9
33
|
|
|
10
34
|
export const BLOCK_GRADIENTS: Record<string, string> = {
|
|
@@ -24,170 +48,51 @@ export const BLOCK_GRADIENTS: Record<string, string> = {
|
|
|
24
48
|
page: "linear-gradient(135deg, #f0e8d8 0%, #e8dcc8 50%, #e0d0b8 100%)",
|
|
25
49
|
};
|
|
26
50
|
|
|
27
|
-
// ──
|
|
51
|
+
// ── Compact wrappers that render the full card icon at a smaller size ──
|
|
52
|
+
//
|
|
53
|
+
// Card icons are 220×120 (landscape ≈11:6). `size` here is the HEIGHT in px;
|
|
54
|
+
// width is derived automatically to preserve aspect.
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<feDropShadow dx="0" dy="1.5" stdDeviation="1.2" floodColor="rgba(80,40,140,0.3)" />
|
|
39
|
-
</filter>
|
|
40
|
-
</defs>
|
|
41
|
-
<path d="M 6,5 L 34,5 L 34,8 L 33,9.5 L 23,9.5 L 23,33 L 26.5,33 L 28,34.5 L 28,37 L 12,37 L 12,34.5 L 13.5,33 L 17,33 L 17,9.5 L 7,9.5 L 6,8 Z" fill="url(#tGrad)" filter="url(#textDrop)" />
|
|
42
|
-
<path d="M 6,5 L 8,3 L 14,3 L 14,5 Z" fill="url(#tGrad)" opacity="0.85" />
|
|
43
|
-
<path d="M 34,5 L 32,3 L 26,3 L 26,5 Z" fill="url(#tGrad)" opacity="0.85" />
|
|
44
|
-
<path d="M 12,37 L 13,38.5 L 18,38.5 L 17,37 Z" fill="url(#tGrad)" opacity="0.8" />
|
|
45
|
-
<path d="M 28,37 L 27,38.5 L 22,38.5 L 23,37 Z" fill="url(#tGrad)" opacity="0.8" />
|
|
46
|
-
<path d="M 6,5 L 34,5 L 34,6.5 L 6,6.5 Z" fill="white" opacity="0.22" />
|
|
47
|
-
<path d="M 17,9.5 L 19,9.5 L 19,33 L 17,33 Z" fill="white" opacity="0.12" />
|
|
48
|
-
</svg>
|
|
49
|
-
);
|
|
56
|
+
const ASPECT = 220 / 120;
|
|
57
|
+
|
|
58
|
+
function scaleToHeight(size: number): React.CSSProperties {
|
|
59
|
+
return {
|
|
60
|
+
width: Math.round(size * ASPECT),
|
|
61
|
+
height: size,
|
|
62
|
+
display: "inline-block",
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
};
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
export function TextBlockIcon({ size = 28 }: { size?: number }) {
|
|
68
|
+
return <span style={scaleToHeight(size)}><TextBlockCardIcon /></span>;
|
|
69
|
+
}
|
|
52
70
|
export function ImageBlockIcon({ size = 28 }: { size?: number }) {
|
|
53
|
-
return (
|
|
54
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
55
|
-
<defs>
|
|
56
|
-
<linearGradient id="mtnBack" x1="10" y1="8" x2="30" y2="32">
|
|
57
|
-
<stop offset="0%" stopColor="#6abf6a" />
|
|
58
|
-
<stop offset="100%" stopColor="#3d9e3d" />
|
|
59
|
-
</linearGradient>
|
|
60
|
-
<linearGradient id="mtnFront" x1="5" y1="12" x2="25" y2="34">
|
|
61
|
-
<stop offset="0%" stopColor="#4da84d" />
|
|
62
|
-
<stop offset="100%" stopColor="#2d7e2d" />
|
|
63
|
-
</linearGradient>
|
|
64
|
-
<filter id="mtnDrop">
|
|
65
|
-
<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" floodColor="rgba(0,0,0,0.15)" />
|
|
66
|
-
</filter>
|
|
67
|
-
</defs>
|
|
68
|
-
<polygon points="20,6 35,32 5,32" fill="url(#mtnBack)" filter="url(#mtnDrop)" />
|
|
69
|
-
<polygon points="20,6 24,13 16,13" fill="white" opacity="0.5" />
|
|
70
|
-
<polygon points="12,14 26,32 -2,32" fill="url(#mtnFront)" filter="url(#mtnDrop)" />
|
|
71
|
-
<polygon points="12,14 15,19 9,19" fill="white" opacity="0.45" />
|
|
72
|
-
</svg>
|
|
73
|
-
);
|
|
71
|
+
return <span style={scaleToHeight(size)}><ImageBlockCardIcon /></span>;
|
|
74
72
|
}
|
|
75
|
-
|
|
76
73
|
export function ImageGridBlockIcon({ size = 28 }: { size?: number }) {
|
|
77
|
-
return (
|
|
78
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
79
|
-
<defs>
|
|
80
|
-
<linearGradient id="gridFill" x1="0" y1="0" x2="40" y2="40">
|
|
81
|
-
<stop offset="0%" stopColor="#6ea8e8" />
|
|
82
|
-
<stop offset="100%" stopColor="#4080c8" />
|
|
83
|
-
</linearGradient>
|
|
84
|
-
<filter id="gridDrop">
|
|
85
|
-
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="rgba(0,0,0,0.12)" />
|
|
86
|
-
</filter>
|
|
87
|
-
</defs>
|
|
88
|
-
<rect x="4" y="4" width="14" height="14" rx="3" fill="url(#gridFill)" opacity="0.85" filter="url(#gridDrop)" />
|
|
89
|
-
<rect x="22" y="4" width="14" height="14" rx="3" fill="url(#gridFill)" opacity="0.65" filter="url(#gridDrop)" />
|
|
90
|
-
<rect x="4" y="22" width="14" height="14" rx="3" fill="url(#gridFill)" opacity="0.55" filter="url(#gridDrop)" />
|
|
91
|
-
<rect x="22" y="22" width="14" height="14" rx="3" fill="url(#gridFill)" opacity="0.75" filter="url(#gridDrop)" />
|
|
92
|
-
</svg>
|
|
93
|
-
);
|
|
74
|
+
return <span style={scaleToHeight(size)}><ImageGridBlockCardIcon /></span>;
|
|
94
75
|
}
|
|
95
|
-
|
|
96
76
|
export function VideoBlockIcon({ size = 28 }: { size?: number }) {
|
|
97
|
-
return (
|
|
98
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
99
|
-
<defs>
|
|
100
|
-
<linearGradient id="playGrad" x1="12" y1="8" x2="32" y2="32">
|
|
101
|
-
<stop offset="0%" stopColor="#f06060" />
|
|
102
|
-
<stop offset="100%" stopColor="#d83838" />
|
|
103
|
-
</linearGradient>
|
|
104
|
-
<filter id="playDrop">
|
|
105
|
-
<feDropShadow dx="0" dy="1.5" stdDeviation="2" floodColor="rgba(200,50,50,0.3)" />
|
|
106
|
-
</filter>
|
|
107
|
-
</defs>
|
|
108
|
-
<path d="M12,6 L34,20 L12,34 Z" fill="url(#playGrad)" filter="url(#playDrop)" />
|
|
109
|
-
<path d="M12,6 L34,20 L12,20 Z" fill="white" opacity="0.15" />
|
|
110
|
-
</svg>
|
|
111
|
-
);
|
|
77
|
+
return <span style={scaleToHeight(size)}><VideoBlockCardIcon /></span>;
|
|
112
78
|
}
|
|
113
|
-
|
|
114
79
|
export function SpacerBlockIcon({ size = 28 }: { size?: number }) {
|
|
115
|
-
return (
|
|
116
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
117
|
-
<defs>
|
|
118
|
-
<filter id="spacerDrop">
|
|
119
|
-
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="rgba(0,0,0,0.1)" />
|
|
120
|
-
</filter>
|
|
121
|
-
</defs>
|
|
122
|
-
<path d="M20,4 L27,13 L23,13 L23,17 L17,17 L17,13 L13,13 Z" fill="#9898b8" opacity="0.7" filter="url(#spacerDrop)" />
|
|
123
|
-
<path d="M20,36 L27,27 L23,27 L23,23 L17,23 L17,27 L13,27 Z" fill="#9898b8" opacity="0.7" filter="url(#spacerDrop)" />
|
|
124
|
-
<line x1="8" y1="20" x2="32" y2="20" stroke="#b0b0c8" strokeWidth="1.5" strokeDasharray="3 2" opacity="0.5" />
|
|
125
|
-
</svg>
|
|
126
|
-
);
|
|
80
|
+
return <span style={scaleToHeight(size)}><SpacerBlockCardIcon /></span>;
|
|
127
81
|
}
|
|
128
|
-
|
|
129
82
|
export function ButtonBlockIcon({ size = 28 }: { size?: number }) {
|
|
130
|
-
return (
|
|
131
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
132
|
-
<defs>
|
|
133
|
-
<linearGradient id="toggleGrad" x1="0" y1="10" x2="40" y2="30">
|
|
134
|
-
<stop offset="0%" stopColor="#3cc87c" />
|
|
135
|
-
<stop offset="100%" stopColor="#28a85c" />
|
|
136
|
-
</linearGradient>
|
|
137
|
-
<filter id="toggleDrop">
|
|
138
|
-
<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" floodColor="rgba(0,0,0,0.15)" />
|
|
139
|
-
</filter>
|
|
140
|
-
<filter id="knobDrop">
|
|
141
|
-
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="rgba(0,0,0,0.2)" />
|
|
142
|
-
</filter>
|
|
143
|
-
</defs>
|
|
144
|
-
<rect x="3" y="11" width="34" height="18" rx="9" fill="url(#toggleGrad)" filter="url(#toggleDrop)" />
|
|
145
|
-
<rect x="3" y="11" width="34" height="9" rx="9" fill="white" opacity="0.12" />
|
|
146
|
-
<circle cx="28" cy="20" r="7" fill="white" filter="url(#knobDrop)" />
|
|
147
|
-
<circle cx="27" cy="18.5" r="2.5" fill="white" opacity="0.5" />
|
|
148
|
-
</svg>
|
|
149
|
-
);
|
|
83
|
+
return <span style={scaleToHeight(size)}><ButtonBlockCardIcon /></span>;
|
|
150
84
|
}
|
|
151
85
|
|
|
152
|
-
// ── Non-block context icons ──
|
|
86
|
+
// ── Non-block context icons (compact wrappers of the section card icons) ──
|
|
153
87
|
|
|
154
88
|
export function CoverSectionSettingsIcon({ size = 28 }: { size?: number }) {
|
|
155
|
-
return (
|
|
156
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
157
|
-
<defs>
|
|
158
|
-
<linearGradient id="csSettingsGrad" x1="5" y1="5" x2="35" y2="35">
|
|
159
|
-
<stop offset="0%" stopColor="#0d9488" />
|
|
160
|
-
<stop offset="100%" stopColor="#0f766e" />
|
|
161
|
-
</linearGradient>
|
|
162
|
-
</defs>
|
|
163
|
-
<rect x="3" y="3" width="34" height="34" rx="6" fill="url(#csSettingsGrad)" opacity="0.12" />
|
|
164
|
-
<rect x="3" y="3" width="34" height="34" rx="6" stroke="url(#csSettingsGrad)" strokeWidth="1.5" fill="none" opacity="0.4" />
|
|
165
|
-
<rect x="7" y="7" width="26" height="16" rx="2" fill="url(#csSettingsGrad)" opacity="0.2" />
|
|
166
|
-
<rect x="7" y="25" width="26" height="8" rx="2" fill="url(#csSettingsGrad)" opacity="0.35" />
|
|
167
|
-
<line x1="9" y1="24" x2="31" y2="24" stroke="#0d9488" strokeWidth="1" opacity="0.4" strokeDasharray="2 2" />
|
|
168
|
-
</svg>
|
|
169
|
-
);
|
|
89
|
+
return <span style={scaleToHeight(size)}><CoverSectionCardIcon /></span>;
|
|
170
90
|
}
|
|
171
91
|
|
|
92
|
+
/** Plain V2 section (row-level) — uses the Empty Section card icon so the
|
|
93
|
+
* settings panel matches the Add Section modal iconography. */
|
|
172
94
|
export function RowIcon({ size = 28 }: { size?: number }) {
|
|
173
|
-
return (
|
|
174
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
175
|
-
<defs>
|
|
176
|
-
<linearGradient id="rowGrad" x1="4" y1="4" x2="36" y2="36">
|
|
177
|
-
<stop offset="0%" stopColor="#8888b0" />
|
|
178
|
-
<stop offset="100%" stopColor="#6868a0" />
|
|
179
|
-
</linearGradient>
|
|
180
|
-
<filter id="rowDrop">
|
|
181
|
-
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="rgba(0,0,0,0.12)" />
|
|
182
|
-
</filter>
|
|
183
|
-
</defs>
|
|
184
|
-
<rect x="3" y="10" width="34" height="6" rx="2" fill="url(#rowGrad)" opacity="0.6" filter="url(#rowDrop)" />
|
|
185
|
-
<rect x="3" y="20" width="15" height="10" rx="2" fill="url(#rowGrad)" opacity="0.85" filter="url(#rowDrop)" />
|
|
186
|
-
<rect x="22" y="20" width="15" height="10" rx="2" fill="url(#rowGrad)" opacity="0.85" filter="url(#rowDrop)" />
|
|
187
|
-
<rect x="3" y="20" width="15" height="4" rx="2" fill="white" opacity="0.15" />
|
|
188
|
-
<rect x="22" y="20" width="15" height="4" rx="2" fill="white" opacity="0.15" />
|
|
189
|
-
</svg>
|
|
190
|
-
);
|
|
95
|
+
return <span style={scaleToHeight(size)}><EmptySectionV2CardIcon /></span>;
|
|
191
96
|
}
|
|
192
97
|
|
|
193
98
|
export function ColumnIcon({ size = 28 }: { size?: number }) {
|
|
@@ -234,47 +139,15 @@ export function PageIcon({ size = 28 }: { size?: number }) {
|
|
|
234
139
|
// ── Lookup maps ──
|
|
235
140
|
|
|
236
141
|
export function ProjectGridBlockIcon({ size = 28 }: { size?: number }) {
|
|
237
|
-
return (
|
|
238
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
239
|
-
<defs>
|
|
240
|
-
<linearGradient id="pgGrad" x1="5" y1="5" x2="35" y2="35">
|
|
241
|
-
<stop offset="0%" stopColor="#d4880a" />
|
|
242
|
-
<stop offset="100%" stopColor="#b06e08" />
|
|
243
|
-
</linearGradient>
|
|
244
|
-
</defs>
|
|
245
|
-
<rect x="3" y="3" width="34" height="14" rx="2" fill="url(#pgGrad)" opacity="0.9" />
|
|
246
|
-
<rect x="3" y="21" width="16" height="16" rx="2" fill="url(#pgGrad)" opacity="0.7" />
|
|
247
|
-
<rect x="22" y="21" width="15" height="16" rx="2" fill="url(#pgGrad)" opacity="0.5" />
|
|
248
|
-
</svg>
|
|
249
|
-
);
|
|
142
|
+
return <span style={scaleToHeight(size)}><ProjectGridCardIcon /></span>;
|
|
250
143
|
}
|
|
251
144
|
|
|
252
145
|
export function ParallaxGroupIcon({ size = 28 }: { size?: number }) {
|
|
253
|
-
return (
|
|
254
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
255
|
-
<defs>
|
|
256
|
-
<linearGradient id="pxGrad" x1="5" y1="5" x2="35" y2="35">
|
|
257
|
-
<stop offset="0%" stopColor="#9060d8" />
|
|
258
|
-
<stop offset="100%" stopColor="#7040b8" />
|
|
259
|
-
</linearGradient>
|
|
260
|
-
</defs>
|
|
261
|
-
<rect x="3" y="3" width="34" height="10" rx="2" fill="url(#pxGrad)" opacity="0.9" />
|
|
262
|
-
<rect x="3" y="16" width="34" height="10" rx="2" fill="url(#pxGrad)" opacity="0.6" />
|
|
263
|
-
<rect x="3" y="29" width="34" height="8" rx="2" fill="url(#pxGrad)" opacity="0.35" />
|
|
264
|
-
<path d="M18 6 L24 8 L18 10 Z" fill="white" opacity="0.5" />
|
|
265
|
-
<path d="M18 19 L24 21 L18 23 Z" fill="white" opacity="0.5" />
|
|
266
|
-
</svg>
|
|
267
|
-
);
|
|
146
|
+
return <span style={scaleToHeight(size)}><ParallaxGroupCardIcon /></span>;
|
|
268
147
|
}
|
|
269
148
|
|
|
270
149
|
export function CustomSectionInstanceIcon({ size = 28 }: { size?: number }) {
|
|
271
|
-
return (
|
|
272
|
-
<svg width={size} height={size} viewBox="0 0 40 40" fill="none">
|
|
273
|
-
<rect x="3" y="6" width="34" height="28" rx="4" stroke="#8b5cf6" strokeWidth="2" fill="none" opacity="0.7" />
|
|
274
|
-
<path d="M16 17a3 3 0 0 0 4.5.32l1.8-1.8a3 3 0 0 0-4.24-4.24l-1.03 1.03" stroke="#8b5cf6" strokeWidth="1.8" strokeLinecap="round" fill="none" />
|
|
275
|
-
<path d="M24 23a3 3 0 0 0-4.5-.32l-1.8 1.8a3 3 0 0 0 4.24 4.24l1.03-1.03" stroke="#8b5cf6" strokeWidth="1.8" strokeLinecap="round" fill="none" />
|
|
276
|
-
</svg>
|
|
277
|
-
);
|
|
150
|
+
return <span style={scaleToHeight(size)}><SavedSectionCardIcon /></span>;
|
|
278
151
|
}
|
|
279
152
|
|
|
280
153
|
export const BLOCK_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>> = {
|