@morphika/andami 0.2.26 → 0.4.0

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.
Files changed (81) hide show
  1. package/app/admin/pages/[slug]/page.tsx +41 -47
  2. package/app/api/admin/assets/scan/route.ts +40 -13
  3. package/app/api/admin/custom-sections/[slug]/route.ts +4 -1
  4. package/app/api/admin/custom-sections/route.ts +4 -1
  5. package/app/api/admin/pages/[slug]/route.ts +7 -1
  6. package/app/api/admin/pages/route.ts +4 -1
  7. package/app/api/admin/r2/connect/route.ts +19 -1
  8. package/app/api/admin/r2/disconnect/route.ts +3 -0
  9. package/app/api/admin/r2/rename/route.ts +52 -13
  10. package/app/api/admin/r2/upload-url/route.ts +8 -1
  11. package/app/api/admin/settings/route.ts +4 -1
  12. package/app/api/admin/styles/route.ts +4 -1
  13. package/components/admin/styles/GridLayoutEditor.tsx +46 -46
  14. package/components/blocks/BlockRenderer.tsx +15 -2
  15. package/components/blocks/CoverSectionRenderer.tsx +75 -3
  16. package/components/blocks/ImageGridBlockRenderer.tsx +17 -11
  17. package/components/blocks/ParallaxGroupRenderer.tsx +45 -10
  18. package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
  19. package/components/blocks/ShaderCanvas.tsx +10 -6
  20. package/components/builder/BlockCardIcons.tsx +227 -0
  21. package/components/builder/BlockLivePreview.tsx +5 -0
  22. package/components/builder/BlockTypePicker.tsx +36 -63
  23. package/components/builder/BuilderCanvas.tsx +6 -2
  24. package/components/builder/ColumnDragOverlay.tsx +3 -3
  25. package/components/builder/CoverRowResizeHandle.tsx +5 -2
  26. package/components/builder/CoverSectionCanvas.tsx +45 -52
  27. package/components/builder/DndWrapper.tsx +1 -1
  28. package/components/builder/InsertionLines.tsx +1 -1
  29. package/components/builder/ParallaxGroupCanvas.tsx +12 -71
  30. package/components/builder/ReadOnlyFrame.tsx +4 -23
  31. package/components/builder/SectionCardIcons.tsx +320 -0
  32. package/components/builder/SectionEditorBar.tsx +17 -12
  33. package/components/builder/SectionTypePicker.tsx +34 -138
  34. package/components/builder/SectionV2Canvas.tsx +1 -1
  35. package/components/builder/SectionV2Column.tsx +19 -30
  36. package/components/builder/SettingsPanel.tsx +8 -32
  37. package/components/builder/SortableBlock.tsx +42 -50
  38. package/components/builder/SortableRow.tsx +207 -19
  39. package/components/builder/blockStyles.tsx +59 -180
  40. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
  41. package/components/builder/editors/index.ts +1 -0
  42. package/components/builder/iconPrimitives.tsx +78 -0
  43. package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
  44. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
  45. package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
  46. package/components/builder/live-preview/index.ts +1 -0
  47. package/components/builder/settings-panel/BlockSettings.tsx +7 -0
  48. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  49. package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
  50. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  51. package/lib/animation/enter-types.ts +1 -0
  52. package/lib/animation/hover-effect-types.ts +1 -0
  53. package/lib/assets.ts +17 -2
  54. package/lib/builder/block-registrations.ts +268 -0
  55. package/lib/builder/block-registry.ts +195 -0
  56. package/lib/builder/constants.ts +22 -15
  57. package/lib/builder/defaults.ts +21 -0
  58. package/lib/builder/format.ts +25 -0
  59. package/lib/builder/history.ts +0 -3
  60. package/lib/builder/index.ts +16 -0
  61. package/lib/builder/layout-styles.ts +1 -1
  62. package/lib/builder/registry.ts +44 -0
  63. package/lib/builder/section-visibility.ts +36 -0
  64. package/lib/builder/serializer/normalizers.ts +15 -6
  65. package/lib/builder/serializer/serializers.ts +3 -3
  66. package/lib/builder/store-blocks.ts +16 -9
  67. package/lib/builder/store-cover.ts +76 -8
  68. package/lib/builder/store-sections.ts +1 -1
  69. package/lib/builder/store.ts +0 -2
  70. package/lib/builder/types.ts +9 -5
  71. package/lib/csrf.ts +31 -0
  72. package/lib/sanity/types.ts +54 -2
  73. package/lib/security.ts +50 -0
  74. package/lib/version.ts +1 -1
  75. package/package.json +1 -1
  76. package/sanity/schemas/blocks/index.ts +2 -1
  77. package/sanity/schemas/blocks/projectCarouselBlock.ts +218 -0
  78. package/sanity/schemas/index.ts +4 -1
  79. package/sanity/schemas/objects/coverSection.ts +35 -3
  80. package/sanity/schemas/pageSectionV2.ts +1 -0
  81. 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 #93278f` }
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(147, 39, 143, 0.4)` }
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 — wide pill aligned top-left outside the row */}
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-l-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
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: "linear-gradient(170deg, rgba(38,38,48,0.97) 0%, rgba(28,28,36,0.98) 100%)",
270
- boxShadow: "0 4px 16px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06)",
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: "rgba(200,160,220,0.9)" }}>
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] text-white/50 hover:text-white/85 transition-colors"
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] text-white/50 hover:text-white/85 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
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] text-white/50 hover:text-white/85 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
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] text-white/50 hover:text-white/85 transition-colors py-0.5"
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 className="text-white/30">+</span> {addColumnLabel}
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] text-white/50 hover:text-red-300 transition-colors py-0.5"
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 className="text-white/30">-</span> Delete
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,33 @@
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
+ ProjectCarouselCardIcon,
29
+ ProjectGridCardIcon,
30
+ SavedSectionCardIcon,
31
+ } from "./SectionCardIcons";
32
+
8
33
  // ── Gradient backgrounds per block type ──
9
34
 
10
35
  export const BLOCK_GRADIENTS: Record<string, string> = {
@@ -24,170 +49,51 @@ export const BLOCK_GRADIENTS: Record<string, string> = {
24
49
  page: "linear-gradient(135deg, #f0e8d8 0%, #e8dcc8 50%, #e0d0b8 100%)",
25
50
  };
26
51
 
27
- // ── SVG Icon Components ──
52
+ // ── Compact wrappers that render the full card icon at a smaller size ──
53
+ //
54
+ // Card icons are 220×120 (landscape ≈11:6). `size` here is the HEIGHT in px;
55
+ // width is derived automatically to preserve aspect.
28
56
 
29
- export function TextBlockIcon({ size = 28 }: { size?: number }) {
30
- return (
31
- <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
32
- <defs>
33
- <linearGradient id="tGrad" x1="10" y1="2" x2="30" y2="38">
34
- <stop offset="0%" stopColor="#a08ee0" />
35
- <stop offset="100%" stopColor="#7060b8" />
36
- </linearGradient>
37
- <filter id="textDrop">
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
- );
57
+ const ASPECT = 220 / 120;
58
+
59
+ function scaleToHeight(size: number): React.CSSProperties {
60
+ return {
61
+ width: Math.round(size * ASPECT),
62
+ height: size,
63
+ display: "inline-block",
64
+ flexShrink: 0,
65
+ };
50
66
  }
51
67
 
68
+ export function TextBlockIcon({ size = 28 }: { size?: number }) {
69
+ return <span style={scaleToHeight(size)}><TextBlockCardIcon /></span>;
70
+ }
52
71
  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
- );
72
+ return <span style={scaleToHeight(size)}><ImageBlockCardIcon /></span>;
74
73
  }
75
-
76
74
  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
- );
75
+ return <span style={scaleToHeight(size)}><ImageGridBlockCardIcon /></span>;
94
76
  }
95
-
96
77
  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
- );
78
+ return <span style={scaleToHeight(size)}><VideoBlockCardIcon /></span>;
112
79
  }
113
-
114
80
  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
- );
81
+ return <span style={scaleToHeight(size)}><SpacerBlockCardIcon /></span>;
127
82
  }
128
-
129
83
  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
- );
84
+ return <span style={scaleToHeight(size)}><ButtonBlockCardIcon /></span>;
150
85
  }
151
86
 
152
- // ── Non-block context icons ──
87
+ // ── Non-block context icons (compact wrappers of the section card icons) ──
153
88
 
154
89
  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
- );
90
+ return <span style={scaleToHeight(size)}><CoverSectionCardIcon /></span>;
170
91
  }
171
92
 
93
+ /** Plain V2 section (row-level) — uses the Empty Section card icon so the
94
+ * settings panel matches the Add Section modal iconography. */
172
95
  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
- );
96
+ return <span style={scaleToHeight(size)}><EmptySectionV2CardIcon /></span>;
191
97
  }
192
98
 
193
99
  export function ColumnIcon({ size = 28 }: { size?: number }) {
@@ -234,47 +140,19 @@ export function PageIcon({ size = 28 }: { size?: number }) {
234
140
  // ── Lookup maps ──
235
141
 
236
142
  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
- );
143
+ return <span style={scaleToHeight(size)}><ProjectGridCardIcon /></span>;
144
+ }
145
+
146
+ export function ProjectCarouselBlockIcon({ size = 28 }: { size?: number }) {
147
+ return <span style={scaleToHeight(size)}><ProjectCarouselCardIcon /></span>;
250
148
  }
251
149
 
252
150
  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
- );
151
+ return <span style={scaleToHeight(size)}><ParallaxGroupCardIcon /></span>;
268
152
  }
269
153
 
270
154
  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
- );
155
+ return <span style={scaleToHeight(size)}><SavedSectionCardIcon /></span>;
278
156
  }
279
157
 
280
158
  export const BLOCK_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>> = {
@@ -285,6 +163,7 @@ export const BLOCK_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>>
285
163
  spacerBlock: SpacerBlockIcon,
286
164
  buttonBlock: ButtonBlockIcon,
287
165
  projectGridBlock: ProjectGridBlockIcon,
166
+ projectCarouselBlock: ProjectCarouselBlockIcon,
288
167
  parallaxGroup: ParallaxGroupIcon,
289
168
  coverSection: CoverSectionSettingsIcon,
290
169
  customSectionInstance: CustomSectionInstanceIcon,