@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.
Files changed (61) hide show
  1. package/app/admin/pages/[slug]/page.tsx +39 -45
  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 +11 -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/ShaderCanvas.tsx +10 -6
  19. package/components/builder/BlockCardIcons.tsx +227 -0
  20. package/components/builder/BlockTypePicker.tsx +36 -63
  21. package/components/builder/BuilderCanvas.tsx +6 -2
  22. package/components/builder/ColumnDragOverlay.tsx +3 -3
  23. package/components/builder/CoverRowResizeHandle.tsx +5 -2
  24. package/components/builder/CoverSectionCanvas.tsx +45 -52
  25. package/components/builder/DndWrapper.tsx +1 -1
  26. package/components/builder/InsertionLines.tsx +1 -1
  27. package/components/builder/ParallaxGroupCanvas.tsx +12 -71
  28. package/components/builder/SectionCardIcons.tsx +266 -0
  29. package/components/builder/SectionEditorBar.tsx +17 -12
  30. package/components/builder/SectionTypePicker.tsx +33 -137
  31. package/components/builder/SectionV2Canvas.tsx +1 -1
  32. package/components/builder/SectionV2Column.tsx +19 -30
  33. package/components/builder/SettingsPanel.tsx +8 -32
  34. package/components/builder/SortableBlock.tsx +42 -50
  35. package/components/builder/SortableRow.tsx +207 -19
  36. package/components/builder/blockStyles.tsx +53 -180
  37. package/components/builder/iconPrimitives.tsx +78 -0
  38. package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
  39. package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
  40. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  41. package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
  42. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  43. package/lib/assets.ts +17 -2
  44. package/lib/builder/constants.ts +22 -15
  45. package/lib/builder/format.ts +25 -0
  46. package/lib/builder/history.ts +0 -3
  47. package/lib/builder/layout-styles.ts +1 -1
  48. package/lib/builder/section-visibility.ts +36 -0
  49. package/lib/builder/serializer/normalizers.ts +15 -6
  50. package/lib/builder/serializer/serializers.ts +3 -3
  51. package/lib/builder/store-blocks.ts +16 -9
  52. package/lib/builder/store-cover.ts +76 -8
  53. package/lib/builder/store.ts +0 -2
  54. package/lib/builder/types.ts +1 -2
  55. package/lib/csrf.ts +31 -0
  56. package/lib/sanity/types.ts +4 -1
  57. package/lib/security.ts +50 -0
  58. package/lib/version.ts +1 -1
  59. package/package.json +1 -1
  60. package/sanity/schemas/objects/coverSection.ts +35 -3
  61. 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,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
- // ── SVG Icon Components ──
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
- 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
- );
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 }>> = {