@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
@@ -3,8 +3,6 @@
3
3
  import { useBuilderStore } from "../../lib/builder/store";
4
4
  import type { ParallaxGroup, ParallaxSlideV2, PageSectionV2 } from "../../lib/sanity/types";
5
5
  import SectionV2Canvas from "./SectionV2Canvas";
6
- import ParallaxSlideHeader from "./ParallaxSlideHeader";
7
- import { BUILDER_GREEN, BUILDER_VIOLET } from "../../lib/builder/constants";
8
6
  import { DEVICE_HEIGHTS } from "../../lib/builder/types";
9
7
  import { useAssetUrl } from "../../lib/contexts/AssetContext";
10
8
 
@@ -12,12 +10,13 @@ import { useAssetUrl } from "../../lib/contexts/AssetContext";
12
10
  * ParallaxGroupCanvas — renders a ParallaxGroup in the builder canvas.
13
11
  *
14
12
  * Each slide is displayed as a stacked section with:
15
- * - ParallaxSlideHeader (index, bg indicator, reorder, delete)
16
13
  * - SectionV2Canvas (full V2 grid editor reuse)
17
14
  * - Faint background preview when a bg image is set
18
15
  * - Empty state message when slide has no background and no content blocks
19
16
  *
20
- * An "Add Slide" button appears at the bottom.
17
+ * Slide-level chrome (select, reorder, delete) + "+ Slide" live in the side
18
+ * pill rendered by SortableRow. The old per-slide header component and the
19
+ * bottom "Add Slide" button were removed to reduce canvas clutter.
21
20
  *
22
21
  * Session 123: Parallax V2 Phase 2
23
22
  * Session 127: Phase 5 — empty state, slide counter badge, smooth reorder
@@ -41,9 +40,7 @@ export default function ParallaxGroupCanvas({
41
40
  group,
42
41
  onAddBlockTarget,
43
42
  }: ParallaxGroupCanvasProps) {
44
- const store = useBuilderStore();
45
- const selectedRowKey = store.selectedRowKey;
46
- const activeViewport = store.activeViewport || "desktop";
43
+ const activeViewport = useBuilderStore((s) => s.activeViewport) || "desktop";
47
44
  const slidePreviewHeight = DEVICE_HEIGHTS[activeViewport];
48
45
  const assetUrl = useAssetUrl();
49
46
 
@@ -56,40 +53,8 @@ export default function ParallaxGroupCanvas({
56
53
  overflow: "visible",
57
54
  }}
58
55
  >
59
- {/* Group header */}
60
- <div
61
- className="flex items-center gap-2 px-3 py-2 cursor-pointer"
62
- style={{
63
- background: selectedRowKey === group._key
64
- ? "linear-gradient(135deg, #e8deff 0%, #ddd0ff 100%)"
65
- : "linear-gradient(135deg, #f3f0ff 0%, #ede5ff 100%)",
66
- borderBottom: "1px solid rgba(139, 92, 246, 0.15)",
67
- borderRadius: "12px 12px 0 0",
68
- }}
69
- onClick={(e) => {
70
- e.stopPropagation();
71
- store.selectRow(group._key);
72
- }}
73
- >
74
- <span className="text-[11px] font-semibold text-[#8b5cf6]">
75
- ▽ Parallax Showcase
76
- </span>
77
- {/* Slide counter badge */}
78
- <span
79
- className="inline-flex items-center justify-center rounded-full text-[9px] font-bold text-white min-w-[18px] h-[18px] px-1"
80
- style={{ background: BUILDER_VIOLET }}
81
- >
82
- {group.slides.length}
83
- </span>
84
- <div className="flex-1" />
85
- <span className="text-[9px] text-neutral-400 uppercase tracking-wider">
86
- {group.transition_effect}
87
- </span>
88
- </div>
89
-
90
- {/* Slides — CSS transition for smooth reorder */}
56
+ {/* Slides — group header removed; slide management lives in the section pill (SortableRow). */}
91
57
  {group.slides.map((slide, slideIndex) => {
92
- const isSlideSelected = selectedRowKey === slide._key;
93
58
  const bgImagePath = slide.background_type === "image" && slide.background_image
94
59
  ? assetUrl(slide.background_image)
95
60
  : null;
@@ -118,15 +83,8 @@ export default function ParallaxGroupCanvas({
118
83
  transition: "opacity 0.2s ease",
119
84
  }}
120
85
  >
121
- {/* Slide header */}
122
- <ParallaxSlideHeader
123
- slide={slide}
124
- slideIndex={slideIndex}
125
- totalSlides={group.slides.length}
126
- groupKey={group._key}
127
- isSelected={isSlideSelected}
128
- onSelect={() => store.selectRow(slide._key)}
129
- />
86
+ {/* Slide header removed — slide selection / delete / reorder
87
+ moved to the side pill (SortableRow). */}
130
88
 
131
89
  {/* Slide content with optional background preview — 100vh equivalent */}
132
90
  <div className="relative" style={{ minHeight: slidePreviewHeight }}>
@@ -166,10 +124,11 @@ export default function ParallaxGroupCanvas({
166
124
  />
167
125
  )}
168
126
 
169
- {/* Empty state message */}
127
+ {/* Empty state message — anchored to the top so it doesn't
128
+ collide with the centered "+ Add Block" pill on hover. */}
170
129
  {slideEmpty && (
171
130
  <div
172
- className="absolute inset-0 flex items-center justify-center pointer-events-none"
131
+ className="absolute inset-0 flex items-start justify-center pt-3 pointer-events-none"
173
132
  style={{ zIndex: 5 }}
174
133
  >
175
134
  <div className="text-center px-6 py-4 rounded-lg" style={{ background: "rgba(139, 92, 246, 0.06)" }}>
@@ -203,26 +162,8 @@ export default function ParallaxGroupCanvas({
203
162
  );
204
163
  })}
205
164
 
206
- {/* Add Slide button */}
207
- <div
208
- className="flex justify-center py-3"
209
- style={{
210
- background: "linear-gradient(135deg, #f9f7ff 0%, #f3f0ff 100%)",
211
- borderTop: "1px solid rgba(139, 92, 246, 0.1)",
212
- borderRadius: "0 0 12px 12px",
213
- }}
214
- >
215
- <button
216
- className="flex items-center gap-1.5 rounded-lg px-4 py-1.5 text-[11px] font-medium text-white transition-colors hover:opacity-90"
217
- style={{ background: BUILDER_GREEN }}
218
- onClick={(e) => {
219
- e.stopPropagation();
220
- store.addParallaxSlide(group._key);
221
- }}
222
- >
223
- <span className="text-sm leading-none">+</span> Add Slide
224
- </button>
225
- </div>
165
+ {/* Add Slide button removed — adding slides is now done from the
166
+ section pill (SortableRow). */}
226
167
  </div>
227
168
  );
228
169
  }
@@ -0,0 +1,266 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Full-size section card icons (220×120) used in the Add Section modal.
5
+ *
6
+ * Same visual language as `BlockCardIcons.tsx` but violet-tinted to
7
+ * differentiate sections from blocks:
8
+ * - accent: #7500D5 (block equivalent: #4794E2)
9
+ * - ghost fill: #E8DFF2 (block equivalent: #DDE6F5)
10
+ * - neutral stroke:#D6C9E2 (block equivalent: #C9D3E4)
11
+ * - bevel end: #EBEAEF (block equivalent: #E6ECF6)
12
+ *
13
+ * IDs are namespaced per icon (`seSec`, `scSec`, `spgSec`, `spSec`, `sccSec`,
14
+ * `ssSec`) so multiple cards can render side by side without collisions.
15
+ */
16
+
17
+ import { Bg, BgDefs, ShadowFilter, VertBevel } from "./iconPrimitives";
18
+
19
+ // Section-specific colour tokens
20
+ const ACCENT = "#7500D5";
21
+ const GHOST = "#E8DFF2";
22
+ const NEUTRAL_STROKE = "#D6C9E2";
23
+ const BEVEL_END = "#EBEAEF";
24
+
25
+ // ─────────────────────────────────────────────────────────────────────
26
+ // Empty Section — frame + 4 ghost columns
27
+ // ─────────────────────────────────────────────────────────────────────
28
+ export function EmptySectionV2CardIcon() {
29
+ return (
30
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
31
+ <defs>
32
+ <BgDefs prefix="seSec" />
33
+ <ShadowFilter id="shadSeSec" />
34
+ <VertBevel id="bevelSeSec" endColor={BEVEL_END} />
35
+ </defs>
36
+ <Bg prefix="seSec" />
37
+
38
+ <g filter="url(#shadSeSec)">
39
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
40
+ fill="url(#bevelSeSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
41
+ </g>
42
+ <g fill={GHOST}>
43
+ <path d="M51.3,24.9H33.4c-1.4,0-2.5,1.1-2.5,2.5v63.3c0,1.4,1.1,2.5,2.5,2.5h17.9c1.4,0,2.5-1.1,2.5-2.5V27.4C53.8,26.1,52.6,24.9,51.3,24.9z" />
44
+ <path d="M78.3,24.9H60.4c-1.4,0-2.5,1.1-2.5,2.5v63.3c0,1.4,1.1,2.5,2.5,2.5h17.9c1.4,0,2.5-1.1,2.5-2.5V27.4C80.8,26.1,79.7,24.9,78.3,24.9z" />
45
+ <path d="M105.1,24.9H87.2c-1.4,0-2.5,1.1-2.5,2.5v63.3c0,1.4,1.1,2.5,2.5,2.5h17.9c1.4,0,2.5-1.1,2.5-2.5V27.4C107.6,26.1,106.5,24.9,105.1,24.9z" />
46
+ <path d="M132.2,24.9h-17.9c-1.4,0-2.5,1.1-2.5,2.5v63.3c0,1.4,1.1,2.5,2.5,2.5h17.9c1.4,0,2.5-1.1,2.5-2.5V27.4C134.7,26.1,133.6,24.9,132.2,24.9z" />
47
+ </g>
48
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
49
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeDasharray="3,3" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ // ─────────────────────────────────────────────────────────────────────
55
+ // Cover Section — frame + vertical + horizontal dimension arrows
56
+ // ─────────────────────────────────────────────────────────────────────
57
+ export function CoverSectionCardIcon() {
58
+ return (
59
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
60
+ <defs>
61
+ <BgDefs prefix="scSec" />
62
+ <ShadowFilter id="shadScSec" />
63
+ <VertBevel id="bevelScSec" endColor={BEVEL_END} />
64
+ </defs>
65
+ <Bg prefix="scSec" />
66
+
67
+ <g filter="url(#shadScSec)">
68
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
69
+ fill="url(#bevelScSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
70
+ </g>
71
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
72
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeDasharray="3,3" />
73
+
74
+ {/* Vertical height arrow */}
75
+ <g fill={ACCENT} stroke={ACCENT} strokeMiterlimit="10">
76
+ <line x1="82.4" y1="30.1" x2="82.4" y2="88.8" />
77
+ <polygon points="82.4,23 78.4,33 82.4,30.6 86.5,33" />
78
+ <polygon points="82.4,95.9 78.4,85.9 82.4,88.3 86.5,85.9" />
79
+ </g>
80
+
81
+ {/* Horizontal width arrow */}
82
+ <g fill={ACCENT} stroke={ACCENT} strokeMiterlimit="10">
83
+ <line x1="127.9" y1="59.4" x2="34.5" y2="59.4" />
84
+ <polygon points="135.4,59.4 125.4,55.4 127.8,59.4 125.4,63.5" />
85
+ <polygon points="29.4,59.4 39.4,55.4 37,59.4 39.4,63.5" />
86
+ </g>
87
+ </svg>
88
+ );
89
+ }
90
+
91
+ // ─────────────────────────────────────────────────────────────────────
92
+ // Project Grid — 5 masonry tiles
93
+ // ─────────────────────────────────────────────────────────────────────
94
+ export function ProjectGridCardIcon() {
95
+ return (
96
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
97
+ <defs>
98
+ <BgDefs prefix="spgSec" />
99
+ <ShadowFilter id="shadSpgSec" />
100
+ <VertBevel id="tileBevelSpgSec" endColor={BEVEL_END} />
101
+ </defs>
102
+ <Bg prefix="spgSec" />
103
+
104
+ {/* 5 masonry tiles with shadow */}
105
+ <g filter="url(#shadSpgSec)">
106
+ <path d="M25.8,19h30.8c1,0,1.8,0.8,1.8,1.8v76.9c0,1-0.8,1.8-1.8,1.8H25.8c-1,0-1.8-0.8-1.8-1.8V20.8C24,19.8,24.8,19,25.8,19z"
107
+ fill="url(#tileBevelSpgSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.6" />
108
+ <path d="M67.5,19h30.8c1,0,1.8,0.8,1.8,1.8v38.4c0,1-0.8,1.8-1.8,1.8H67.5c-1,0-1.8-0.8-1.8-1.8V20.8C65.6,19.8,66.5,19,67.5,19z"
109
+ fill="url(#tileBevelSpgSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.6" />
110
+ <path d="M109.1,19h30.8c1,0,1.8,0.8,1.8,1.8v53.1c0,1-0.8,1.8-1.8,1.8h-30.8c-1,0-1.8-0.8-1.8-1.8V20.8C107.3,19.8,108.1,19,109.1,19z"
111
+ fill="url(#tileBevelSpgSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.6" />
112
+ <path d="M66.8,67h30.8c1,0,1.8,0.8,1.8,1.8v29.3c0,1-0.8,1.8-1.8,1.8H66.8c-1,0-1.8-0.8-1.8-1.8V68.8C65,67.8,65.8,67,66.8,67z"
113
+ fill="url(#tileBevelSpgSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.6" />
114
+ <path d="M139.6,99.7h-30.3c-1.2,0-2.1-0.9-2.1-2.1V81.4c0-1.2,0.9-2.1,2.1-2.1h30.3c1.2,0,2.1,0.9,2.1,2.1v16.2C141.6,98.8,140.7,99.7,139.6,99.7z"
115
+ fill="url(#tileBevelSpgSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.6" />
116
+ </g>
117
+
118
+ {/* Inner ghost content */}
119
+ <g fill={GHOST}>
120
+ <path d="M28.7,22.6h25c0.6,0,1.1,0.5,1.1,1.1v54.5c0,0.6-0.5,1.1-1.1,1.1h-25c-0.6,0-1.1-0.5-1.1-1.1V23.7C27.7,23.1,28.1,22.6,28.7,22.6z" />
121
+ <path d="M70.4,22.6h25c0.6,0,1.1,0.5,1.1,1.1v23.4c0,0.6-0.5,1.1-1.1,1.1h-25c-0.6,0-1.1-0.5-1.1-1.1V23.7C69.3,23.1,69.7,22.6,70.4,22.6z" />
122
+ <path d="M112,22.6h25c0.6,0,1.1,0.5,1.1,1.1v38.1c0,0.6-0.5,1.1-1.1,1.1h-25c-0.6,0-1.1-0.5-1.1-1.1V23.7C110.9,23.1,111.3,22.6,112,22.6z" />
123
+ <path d="M69.7,70.7h25c0.6,0,1.1,0.5,1.1,1.1v17.9c0,0.6-0.5,1.1-1.1,1.1h-25c-0.6,0-1.1-0.5-1.1-1.1V71.8C68.6,71.2,69.2,70.7,69.7,70.7z" />
124
+ <path d="M137.5,96.2h-25.7c-0.7,0-1.4-0.6-1.4-1.4V84c0-0.7,0.6-1.4,1.4-1.4h25.7c0.7,0,1.4,0.6,1.4,1.4v10.8C138.7,95.6,138.2,96.2,137.5,96.2z" />
125
+ <path d="M28.6,83h18.1c0.5,0,0.9,0.4,0.9,0.9v0.9c0,0.5-0.4,0.9-0.9,0.9H28.6c-0.5,0-0.9-0.4-0.9-0.9v-0.9C27.7,83.4,28,83,28.6,83z" />
126
+ <path d="M28.6,88.5h10.9c0.5,0,0.9,0.4,0.9,0.9v0.9c0,0.5-0.4,0.9-0.9,0.9H28.6c-0.5,0-0.9-0.4-0.9-0.9v-0.9C27.7,88.9,28,88.5,28.6,88.5z" />
127
+ </g>
128
+
129
+ {/* Dashed violet outlines on all tiles */}
130
+ <g fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3">
131
+ <path d="M25.8,19h30.8c1,0,1.8,0.8,1.8,1.8v76.9c0,1-0.8,1.8-1.8,1.8H25.8c-1,0-1.8-0.8-1.8-1.8V20.8C24,19.8,24.8,19,25.8,19z" />
132
+ <path d="M67.5,19h30.8c1,0,1.8,0.8,1.8,1.8v38.4c0,1-0.8,1.8-1.8,1.8H67.5c-1,0-1.8-0.8-1.8-1.8V20.8C65.6,19.8,66.5,19,67.5,19z" />
133
+ <path d="M109.1,19h30.8c1,0,1.8,0.8,1.8,1.8v53.1c0,1-0.8,1.8-1.8,1.8h-30.8c-1,0-1.8-0.8-1.8-1.8V20.8C107.3,19.8,108.1,19,109.1,19z" />
134
+ <path d="M66.8,67h30.8c1,0,1.8,0.8,1.8,1.8v29.3c0,1-0.8,1.8-1.8,1.8H66.8c-1,0-1.8-0.8-1.8-1.8V68.8C65,67.8,65.8,67,66.8,67z" />
135
+ <path d="M139.6,99.7h-30.3c-1.2,0-2.1-0.9-2.1-2.1V81.4c0-1.2,0.9-2.1,2.1-2.1h30.3c1.2,0,2.1,0.9,2.1,2.1v16.2C141.6,98.8,140.7,99.7,139.6,99.7z" />
136
+ </g>
137
+ </svg>
138
+ );
139
+ }
140
+
141
+ // ─────────────────────────────────────────────────────────────────────
142
+ // Parallax Group — 3 stacked slides with image content
143
+ // ─────────────────────────────────────────────────────────────────────
144
+ export function ParallaxGroupCardIcon() {
145
+ return (
146
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
147
+ <defs>
148
+ <BgDefs prefix="spSec" />
149
+ <ShadowFilter id="shadSpSec" />
150
+ <VertBevel id="slideBevelSpSec" endColor={BEVEL_END} />
151
+ </defs>
152
+ <Bg prefix="spSec" />
153
+
154
+ {/* Slide 1 (back/smallest) */}
155
+ <g filter="url(#shadSpSec)">
156
+ <path d="M62.1,18.7H105c0.5,0,0.9,0.3,0.9,0.7v19.5c0,0.4-0.4,0.7-0.9,0.7H62.1c-0.5,0-0.9-0.3-0.9-0.7V19.4C61.2,19,61.6,18.7,62.1,18.7z"
157
+ fill="url(#slideBevelSpSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
158
+ </g>
159
+ <path d="M66.8,22.6h33.5c0.2,0,0.5,0.1,0.5,0.3v12.4c0,0.2-0.2,0.3-0.5,0.3H66.8c-0.2,0-0.5-0.1-0.5-0.3V22.9C66.3,22.7,66.5,22.6,66.8,22.6z"
160
+ fill={GHOST} />
161
+ <ellipse cx="95.2" cy="26.5" rx="1.5" ry="1.5" fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
162
+ <path d="M69.1,32.7l3.9-4.2c0.3-0.3,0.7-0.3,1,0l2.2,2.1c0.3,0.3,0.7,0.3,1,0l2-1.9c0.2-0.2,0.6-0.3,0.9-0.1l6,3.9c0.6,0.3,0.3,1.2-0.4,1.2H69.6C69.1,33.8,68.7,33.1,69.1,32.7z"
163
+ fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
164
+ <path d="M62.6,18.7h41.8c0.5,0,0.9,0.3,0.9,0.7v19.5c0,0.4-0.4,0.7-0.9,0.7H62.6c-0.5,0-0.9-0.3-0.9-0.7V19.4C61.8,19,62.1,18.7,62.6,18.7z"
165
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" />
166
+
167
+ {/* Slide 2 (middle) */}
168
+ <g filter="url(#shadSpSec)">
169
+ <path d="M49.6,34h67.9c0.8,0,1.4,0.5,1.4,1.1v30.8c0,0.6-0.7,1.1-1.4,1.1H49.6c-0.8,0-1.4-0.5-1.4-1.1V35.1C48.2,34.5,48.8,34,49.6,34z"
170
+ fill="url(#slideBevelSpSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
171
+ </g>
172
+ <path d="M55.7,39.4h55.7c0.4,0,0.8,0.2,0.8,0.5v21.7c0,0.3-0.3,0.5-0.8,0.5H55.7c-0.4,0-0.8-0.2-0.8-0.5V39.9C55,39.6,55.3,39.4,55.7,39.4z"
173
+ fill={GHOST} />
174
+ <ellipse cx="102.2" cy="44.3" rx="2.4" ry="2.4" fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
175
+ <path d="M60.3,56.6l8.7-9.8c0.6-0.6,1.6-0.7,2.2,0l5,5c0.6,0.6,1.5,0.6,2.1,0l4.4-4.4c0.5-0.5,1.3-0.6,1.9-0.2l13.3,9.2c1.2,0.8,0.6,2.8-0.9,2.8H61.4C60.1,59.1,59.4,57.5,60.3,56.6z"
176
+ fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
177
+ <path d="M50.4,34h66.3c0.8,0,1.4,0.5,1.4,1.1v30.8c0,0.6-0.6,1.1-1.4,1.1H50.4c-0.8,0-1.4-0.5-1.4-1.1V35.1C49.1,34.5,49.6,34,50.4,34z"
178
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" />
179
+
180
+ {/* Slide 3 (front/largest) */}
181
+ <g filter="url(#shadSpSec)">
182
+ <path d="M34,51.6h99c1.1,0,2,0.7,2,1.6v45c0,0.9-1,1.6-2,1.6H34c-1.1-0.1-2-0.8-2-1.6v-45C32,52.3,32.9,51.6,34,51.6z"
183
+ fill="url(#slideBevelSpSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
184
+ </g>
185
+ <path d="M41.3,58.1h84.5c0.6,0,1.1,0.4,1.1,0.8v33.5c0,0.5-0.5,0.8-1.1,0.8H41.3c-0.6,0-1.1-0.4-1.1-0.8V58.9C40.2,58.4,40.7,58.1,41.3,58.1z"
186
+ fill={GHOST} />
187
+ <ellipse cx="110.8" cy="66.7" rx="3.5" ry="3.5" fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
188
+ <path d="M47.6,84.9l12.7-14.4c0.9-0.9,2.3-1,3.2-0.1l7.2,7.3c0.9,0.9,2.2,0.9,3.1,0l6.4-6.4c0.7-0.7,1.9-0.9,2.8-0.2l19.3,13.4c1.8,1.2,0.9,4.1-1.2,4.1h-52C47.4,88.6,46.4,86.3,47.6,84.9z"
189
+ fill="#FFFFFF" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
190
+ <path d="M35.2,51.6h96.6c1.1,0,2,0.7,2,1.6v45c0,0.9-0.9,1.6-2,1.6H35.2c-1.1-0.1-2-0.8-2-1.6v-45C33.2,52.3,34.1,51.6,35.2,51.6z"
191
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" />
192
+
193
+ {/* Scroll arrow on the right */}
194
+ <g fill={ACCENT} stroke={ACCENT} strokeMiterlimit="10">
195
+ <line x1="150.1" y1="17.6" x2="150.1" y2="88.3" />
196
+ <polygon points="150.1,96.8 145.3,84.8 150.1,87.7 155,84.8" />
197
+ </g>
198
+ </svg>
199
+ );
200
+ }
201
+
202
+ // ─────────────────────────────────────────────────────────────────────
203
+ // Create Custom Section — frame + circle + plus
204
+ // ─────────────────────────────────────────────────────────────────────
205
+ export function CreateCustomSectionCardIcon() {
206
+ return (
207
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
208
+ <defs>
209
+ <BgDefs prefix="sccSec" />
210
+ <ShadowFilter id="shadSccSec" />
211
+ <VertBevel id="bevelSccSec" endColor={BEVEL_END} />
212
+ </defs>
213
+ <Bg prefix="sccSec" />
214
+
215
+ <g filter="url(#shadSccSec)">
216
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
217
+ fill="url(#bevelSccSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
218
+ </g>
219
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
220
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" />
221
+
222
+ <circle cx="82.8" cy="59.2" r="16.9" fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" />
223
+ <line x1="82.8" y1="50.2" x2="82.8" y2="67.7" fill="none" stroke={ACCENT} strokeWidth="3" strokeLinecap="round" strokeMiterlimit="10" />
224
+ <line x1="74" y1="58.9" x2="91.5" y2="58.9" fill="none" stroke={ACCENT} strokeWidth="3" strokeLinecap="round" strokeMiterlimit="10" />
225
+ </svg>
226
+ );
227
+ }
228
+
229
+ // ─────────────────────────────────────────────────────────────────────
230
+ // Saved Custom Section — frame + save/download icon
231
+ // ─────────────────────────────────────────────────────────────────────
232
+ export function SavedSectionCardIcon() {
233
+ return (
234
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
235
+ <defs>
236
+ <BgDefs prefix="ssSec" />
237
+ <ShadowFilter id="shadSsSec" />
238
+ <VertBevel id="bevelSsSec" endColor={BEVEL_END} />
239
+ </defs>
240
+ <Bg prefix="ssSec" />
241
+
242
+ <g filter="url(#shadSsSec)">
243
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
244
+ fill="url(#bevelSsSec)" stroke={NEUTRAL_STROKE} strokeWidth="0.7" />
245
+ </g>
246
+ <path d="M26.8,19h112.7c1.3,0,2.3,1.2,2.3,2.7v75c0,1.5-1.1,2.7-2.3,2.7H26.8c-1.3-0.1-2.3-1.3-2.3-2.7V21.6C24.5,20.2,25.5,19,26.8,19z"
247
+ fill="none" stroke={ACCENT} strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" />
248
+
249
+ {/* Save/download icon */}
250
+ <path fill={ACCENT}
251
+ d="M69.4,39.5c-0.7,0-1.2,0.5-1.2,1.2v34.2c0,0.7,0.5,1.2,1.2,1.2h28.5c0.7,0,1.2-0.5,1.2-1.2V49.7c0-0.3-0.1-0.6-0.4-0.8l-9.1-9.1h0c-0.2-0.2-0.5-0.3-0.8-0.3L69.4,39.5z M70.6,41.9h17.8l8.3,8.4v23.5H70.6V41.9z M83.7,47.6c-0.7,0-1.2,0.5-1.2,1.2v11.5l-2-2h0C80.3,58.1,80,58,79.6,58c-0.3,0-0.6,0.1-0.9,0.4c-0.5,0.5-0.5,1.2,0,1.7l4,4c0.2,0.2,0.4,0.3,0.8,0.3c0.3,0,0.6-0.1,0.8-0.3l4-4c0.5-0.5,0.5-1.2,0-1.7c-0.5-0.5-1.2-0.5-1.7,0l-2,2V48.8C84.9,48.2,84.3,47.6,83.7,47.6L83.7,47.6z M78.1,65.5c-0.7,0-1.2,0.5-1.2,1.2c0,0.7,0.5,1.2,1.2,1.2h11.2c0.7,0,1.2-0.5,1.2-1.2c0-0.7-0.5-1.2-1.2-1.2H78.1z" />
252
+ </svg>
253
+ );
254
+ }
255
+
256
+ // ─────────────────────────────────────────────────────────────────────
257
+ // Lookup map for the Add Section modal
258
+ // ─────────────────────────────────────────────────────────────────────
259
+ export const SECTION_CARD_ICONS: Record<string, React.FC> = {
260
+ "empty-v2": EmptySectionV2CardIcon,
261
+ coverSection: CoverSectionCardIcon,
262
+ projectGridBlock: ProjectGridCardIcon,
263
+ parallaxGroup: ParallaxGroupCardIcon,
264
+ createCustom: CreateCustomSectionCardIcon,
265
+ savedCustom: SavedSectionCardIcon,
266
+ };
@@ -119,13 +119,12 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
119
119
  return (
120
120
  <>
121
121
  <div
122
- className="flex items-center justify-between px-4 py-2 border-b border-[#2a2a2a]"
122
+ className="grid grid-cols-[1fr_auto_1fr] items-center px-4 py-2 border-b border-[#2a2a2a]"
123
123
  style={{ backgroundColor: "#1a1a1a" }}
124
124
  >
125
- {/* Left: breadcrumbs + section name input */}
126
- <div className="flex items-center gap-3">
127
- {/* Breadcrumbs */}
128
- <div className="flex items-center gap-1.5 text-xs mr-1">
125
+ {/* LEFT breadcrumbs */}
126
+ <div className="flex items-center gap-3 justify-self-start">
127
+ <div className="flex items-center gap-1.5 text-xs">
129
128
  <a
130
129
  href={parentListUrl}
131
130
  className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer"
@@ -145,24 +144,30 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
145
144
  {isEditing ? "Edit Section" : "New Section"}
146
145
  </span>
147
146
  </div>
148
-
149
147
  <div className="w-px h-4 bg-[#3a3a3a]" />
148
+ </div>
150
149
 
150
+ {/* CENTER — section name input (identity of the thing being edited) */}
151
+ <div className="flex items-center justify-self-center">
151
152
  <input
152
153
  ref={inputRef}
153
154
  type="text"
154
155
  value={name}
155
156
  onChange={(e) => setName(e.target.value)}
156
157
  placeholder="Section name..."
157
- className="bg-[#2a2a2a] text-white text-sm px-3 py-1.5 rounded border border-[#3a3a3a] focus:border-[#076bff] focus:outline-none w-64 transition-colors"
158
+ className="bg-[#2a2a2a] text-white text-sm px-3 py-1.5 rounded border border-[#3a3a3a] focus:border-[#076bff] focus:outline-none w-64 transition-colors text-center"
158
159
  />
159
160
  </div>
160
161
 
161
- {/* Right: actions */}
162
- <div className="flex items-center gap-2">
163
- {saveError && (
164
- <span className="text-xs text-red-400 mr-2">{saveError}</span>
165
- )}
162
+ {/* RIGHT — notification slot + actions */}
163
+ <div className="flex items-center gap-2 justify-self-end">
164
+ {/* Notification slot: save error takes precedence over unsaved-changes */}
165
+ {saveError ? (
166
+ <span className="text-xs text-red-400 mr-1">{saveError}</span>
167
+ ) : isDirty ? (
168
+ <span className="text-xs text-amber-400 animate-pulse mr-1">Unsaved changes</span>
169
+ ) : null}
170
+
166
171
  {/* Delete button — only for existing sections */}
167
172
  {isEditing && (
168
173
  <button