@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
@@ -0,0 +1,227 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Full-size block card icons (220×120) used in the Add Block modal.
5
+ *
6
+ * Visual language: soft grid background with a 24-rect brick overlay that
7
+ * fades to #F4F4F4 on the right (matches the card's clean gray). 3D white
8
+ * shapes with cool-blue bevel + vector drop shadow. Dashed blue (#4794E2)
9
+ * construction guides on top.
10
+ *
11
+ * IDs are namespaced per icon (`tBlk`, `iBlk`, `igBlk`, `vBlk`, `sBlk`,
12
+ * `bBlk`) so multiple cards can render side by side without filter /
13
+ * gradient collisions.
14
+ */
15
+
16
+ import { Bg, BgDefs, ShadowFilter, VertBevel } from "./iconPrimitives";
17
+
18
+ // ─────────────────────────────────────────────────────────────────────
19
+ // Text
20
+ // ─────────────────────────────────────────────────────────────────────
21
+ export function TextBlockCardIcon() {
22
+ return (
23
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
24
+ <defs>
25
+ <BgDefs prefix="tBlk" />
26
+ <ShadowFilter id="shadTBlk" />
27
+ <linearGradient id="bevelATBlk" x1="61.7" y1="19.7" x2="61.7" y2="99.7" gradientUnits="userSpaceOnUse">
28
+ <stop offset="0" stopColor="#FFFFFF" />
29
+ <stop offset="1" stopColor="#E6ECF6" />
30
+ </linearGradient>
31
+ <linearGradient id="bevelaTBlk" x1="125.3" y1="44.9" x2="125.2" y2="84.4" gradientUnits="userSpaceOnUse">
32
+ <stop offset="0" stopColor="#FFFFFF" />
33
+ <stop offset="1" stopColor="#E6ECF6" />
34
+ </linearGradient>
35
+ </defs>
36
+ <Bg prefix="tBlk" />
37
+
38
+ {/* Uppercase A — 3D body with shadow */}
39
+ <g filter="url(#shadTBlk)">
40
+ <path fillRule="evenodd" clipRule="evenodd" fill="url(#bevelATBlk)" stroke="#C9D3E4" strokeWidth="0.7"
41
+ d="M93.5,97.8H74.3l-3.1-13.6H51.8l-3.2,13.6H29.8l21.5-81.2h20.6L93.5,97.8z M67.7,69.7c-2.7-12.1-4.9-23.6-6.2-34.4h-0.3c-1.5,11.2-3.5,22.7-6.2,34.4H67.7z" />
42
+ </g>
43
+ {/* A dashed accent */}
44
+ <path fillRule="evenodd" clipRule="evenodd" fill="none" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" opacity="0.69"
45
+ d="M93.5,97.8H74.3l-3.1-13.6H51.8l-3.2,13.6H29.8l21.5-81.2h20.6L93.5,97.8z M67.7,69.7c-2.7-12.1-4.9-23.6-6.2-34.4h-0.3c-1.5,11.2-3.5,22.7-6.2,34.4H67.7z" />
46
+
47
+ {/* Lowercase a — 3D body with shadow */}
48
+ <g filter="url(#shadTBlk)">
49
+ <path fill="url(#bevelaTBlk)" stroke="#C9D3E4" strokeWidth="0.7"
50
+ d="M136.7,98l-0.7-6.2h-0.3c-2.8,3.9-8.1,7.4-15.1,7.4c-10,0-15.1-7.1-15.1-14.2c0-12,10.6-18.5,29.7-18.4v-1c0-4.1-1.1-11.4-11.2-11.4c-4.6,0-9.4,1.4-12.9,3.7l-2-5.9c4.1-2.7,10-4.4,16.3-4.4c15.1,0,18.8,10.3,18.8,20.2v18.5c0,4.3,0.2,8.5,0.8,11.9H136.7z M135.4,72.8c-9.8-0.2-21,1.5-21,11.1c0,5.8,3.9,8.6,8.5,8.6c6.4,0,10.5-4.1,12-8.3c0.3-0.9,0.5-1.9,0.5-2.9V72.8z" />
51
+ </g>
52
+ {/* a dashed accent (matches A) */}
53
+ <path fill="none" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10" strokeDasharray="3,3" opacity="0.69"
54
+ d="M136.7,98l-0.7-6.2h-0.3c-2.8,3.9-8.1,7.4-15.1,7.4c-10,0-15.1-7.1-15.1-14.2c0-12,10.6-18.5,29.7-18.4v-1c0-4.1-1.1-11.4-11.2-11.4c-4.6,0-9.4,1.4-12.9,3.7l-2-5.9c4.1-2.7,10-4.4,16.3-4.4c15.1,0,18.8,10.3,18.8,20.2v18.5c0,4.3,0.2,8.5,0.8,11.9H136.7z M135.4,72.8c-9.8-0.2-21,1.5-21,11.1c0,5.8,3.9,8.6,8.5,8.6c6.4,0,10.5-4.1,12-8.3c0.3-0.9,0.5-1.9,0.5-2.9V72.8z" />
55
+
56
+ {/* Right-side vertical bracket */}
57
+ <g fill="none" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10">
58
+ <line x1="159.7" y1="18.5" x2="159.7" y2="101.4" />
59
+ <line x1="153.4" y1="18.5" x2="166.1" y2="18.5" />
60
+ <line x1="153.4" y1="101.4" x2="166.1" y2="101.4" />
61
+ </g>
62
+ </svg>
63
+ );
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────
67
+ // Image
68
+ // ─────────────────────────────────────────────────────────────────────
69
+ export function ImageBlockCardIcon() {
70
+ return (
71
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
72
+ <defs>
73
+ <BgDefs prefix="iBlk" />
74
+ <ShadowFilter id="shadIBlk" />
75
+ <VertBevel id="bevelIBlk" />
76
+ </defs>
77
+ <Bg prefix="iBlk" />
78
+
79
+ <g filter="url(#shadIBlk)">
80
+ <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"
81
+ fill="url(#bevelIBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
82
+ </g>
83
+ <path d="M31.5,25.3h103.4c0.7,0,1.4,0.7,1.4,1.6v64.4c0,0.9-0.6,1.6-1.4,1.6H31.5c-0.7,0-1.4-0.7-1.4-1.6V26.9C30.1,25.9,30.7,25.3,31.5,25.3z"
84
+ fill="#DDE6F5" stroke="#C9D3E4" strokeWidth="0.5" />
85
+ <ellipse cx="116.3" cy="41.3" rx="5.1" ry="5.1" fill="#FFFFFF" stroke="#4794E2" strokeWidth="2" />
86
+ <path d="M37.1,79.6l20.3-23c1.4-1.5,3.7-1.6,5.2-0.1l11.6,11.7c1.4,1.4,3.6,1.4,5,0L89.5,58c1.2-1.2,3.1-1.4,4.5-0.4l31,21.5c2.9,1.9,1.5,6.5-2,6.5H39.7C36.7,85.5,35.1,81.9,37.1,79.6z"
87
+ fill="#FFFFFF" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10" />
88
+ <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"
89
+ fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3" />
90
+ </svg>
91
+ );
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────────
95
+ // Image Grid
96
+ // ─────────────────────────────────────────────────────────────────────
97
+ export function ImageGridBlockCardIcon() {
98
+ return (
99
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
100
+ <defs>
101
+ <BgDefs prefix="igBlk" />
102
+ <ShadowFilter id="shadIGBlk" />
103
+ <VertBevel id="bevelIGBlk" />
104
+ </defs>
105
+ <Bg prefix="igBlk" />
106
+
107
+ <g filter="url(#shadIGBlk)">
108
+ <rect x="24" y="26" width="58" height="34" rx="2" fill="url(#bevelIGBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
109
+ <rect x="90" y="26" width="58" height="34" rx="2" fill="url(#bevelIGBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
110
+ <rect x="24" y="68" width="58" height="34" rx="2" fill="url(#bevelIGBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
111
+ <rect x="90" y="68" width="58" height="34" rx="2" fill="url(#bevelIGBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
112
+ </g>
113
+ <rect x="30" y="32" width="46" height="22" rx="1.2" fill="#DDE6F5" />
114
+ <rect x="96" y="32" width="46" height="22" rx="1.2" fill="#DDE6F5" />
115
+ <rect x="30" y="74" width="46" height="22" rx="1.2" fill="#DDE6F5" />
116
+ <rect x="96" y="74" width="46" height="22" rx="1.2" fill="#DDE6F5" />
117
+ <g fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3">
118
+ <rect x="24" y="26" width="58" height="34" rx="2" />
119
+ <rect x="90" y="26" width="58" height="34" rx="2" />
120
+ <rect x="24" y="68" width="58" height="34" rx="2" />
121
+ <rect x="90" y="68" width="58" height="34" rx="2" />
122
+ </g>
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ // ─────────────────────────────────────────────────────────────────────
128
+ // Video
129
+ // ─────────────────────────────────────────────────────────────────────
130
+ export function VideoBlockCardIcon() {
131
+ return (
132
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
133
+ <defs>
134
+ <BgDefs prefix="vBlk" />
135
+ <ShadowFilter id="shadVBlk" />
136
+ <VertBevel id="bevelVBlk" />
137
+ </defs>
138
+ <Bg prefix="vBlk" />
139
+
140
+ <g filter="url(#shadVBlk)">
141
+ <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"
142
+ fill="url(#bevelVBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
143
+ </g>
144
+ <path d="M31.5,25.3h103.4c0.7,0,1.4,0.7,1.4,1.6v64.4c0,0.9-0.6,1.6-1.4,1.6H31.5c-0.7,0-1.4-0.7-1.4-1.6V26.9C30.1,25.9,30.7,25.3,31.5,25.3z"
145
+ fill="#DDE6F5" stroke="#C9D3E4" strokeWidth="0.5" />
146
+ <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"
147
+ fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3" />
148
+ <path d="M75.2,45.9l21.3,11.4c2.4,1.2,2.4,4.6,0,5.9L75.2,74.5c-2.3,1.2-4.9-0.5-4.9-2.9V48.8C70.3,46.3,73,44.6,75.2,45.9z"
149
+ fill="#FFFFFF" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10" />
150
+ </svg>
151
+ );
152
+ }
153
+
154
+ // ─────────────────────────────────────────────────────────────────────
155
+ // Spacer (edge case — horizontal gradient, no shadow)
156
+ // ─────────────────────────────────────────────────────────────────────
157
+ export function SpacerBlockCardIcon() {
158
+ return (
159
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
160
+ <defs>
161
+ <BgDefs prefix="sBlk" />
162
+ <linearGradient id="bevelSBlk" x1="15" y1="59" x2="144.7" y2="59" gradientUnits="userSpaceOnUse">
163
+ <stop offset="0" stopColor="#FFFFFF" stopOpacity="0" />
164
+ <stop offset="0.26" stopColor="#F2F5FA" />
165
+ <stop offset="0.79" stopColor="#F2F5FA" />
166
+ <stop offset="1" stopColor="#E6ECF6" stopOpacity="0" />
167
+ </linearGradient>
168
+ </defs>
169
+ <Bg prefix="sBlk" />
170
+
171
+ {/* Main frame — no shadow (edge case: horizontal gradient fill) */}
172
+ <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"
173
+ fill="url(#bevelSBlk)" />
174
+
175
+ {/* Blue up/down arrows */}
176
+ <g fill="#4794E2">
177
+ <path d="M82.7,27v18.8h2V27l2,1.9c0.3,0.3,0.9,0.3,1.2,0l0.2-0.2c0.3-0.3,0.3-0.9,0-1.2l-3.7-3.6c-0.4-0.4-1-0.4-1.4,0l-3.7,3.6c-0.3,0.3-0.3,0.9,0,1.2l0.2,0.2c0.3,0.3,0.9,0.3,1.2,0L82.7,27z" />
178
+ <path d="M86.7,89.7l-2,2v-18c0-0.5-0.4-0.8-0.8-0.8h-0.3c-0.5,0-0.8,0.4-0.8,0.8v18l-2-2c-0.3-0.3-0.9-0.3-1.2,0l-0.2,0.2c-0.3,0.3-0.3,0.9,0,1.2l3.7,3.7c0.2,0.2,0.4,0.3,0.7,0.3s0.5-0.1,0.7-0.3l3.7-3.7c0.3-0.3,0.3-0.9,0-1.2l-0.2-0.2C87.5,89.3,87,89.3,86.7,89.7z" />
179
+ </g>
180
+
181
+ {/* Dashed blue boundary lines */}
182
+ <line x1="22.1" y1="20.9" x2="146.5" y2="20.9" fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3" />
183
+ <line x1="22.1" y1="99.2" x2="146.5" y2="99.2" fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3" />
184
+ </svg>
185
+ );
186
+ }
187
+
188
+ // ─────────────────────────────────────────────────────────────────────
189
+ // Button
190
+ // ─────────────────────────────────────────────────────────────────────
191
+ export function ButtonBlockCardIcon() {
192
+ return (
193
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 120" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">
194
+ <defs>
195
+ <BgDefs prefix="bBlk" />
196
+ <ShadowFilter id="shadBBlk" />
197
+ <VertBevel id="bevelBBlk" />
198
+ </defs>
199
+ <Bg prefix="bBlk" />
200
+
201
+ <g filter="url(#shadBBlk)">
202
+ <path d="M43,39h83.3c12.7,0,22.9,9.2,22.9,20.7l0,0c0,11.5-10.2,20.7-22.9,20.7H43c-12.7,0-22.9-9.2-22.9-20.7l0,0C20.1,48.3,30.4,39,43,39z"
203
+ fill="url(#bevelBBlk)" stroke="#C9D3E4" strokeWidth="0.7" />
204
+ </g>
205
+ <path d="M43,39h83.3c12.7,0,22.9,9.2,22.9,20.7l0,0c0,11.5-10.2,20.7-22.9,20.7H43c-12.7,0-22.9-9.2-22.9-20.7l0,0C20.1,48.3,30.4,39,43,39z"
206
+ fill="none" stroke="#4794E2" strokeWidth="2" strokeDasharray="3,3" />
207
+
208
+ {/* Cursor icon */}
209
+ <g transform="translate(-240 -432)">
210
+ <path fillRule="evenodd" clipRule="evenodd" fill="#FFFFFF" stroke="#4794E2" strokeWidth="2" strokeMiterlimit="10"
211
+ d="M353.1,513.5l8.2,8.2c0.9,0.9,2.5,0.9,3.4,0c0.9-0.9,2.3-2.3,3.1-3.1c0.5-0.5,0.7-1.1,0.7-1.7c0-0.6-0.3-1.3-0.7-1.7l-8.2-8.2l5.6-3.8c0.8-0.5,1.2-1.4,1-2.3c-0.1-0.9-0.8-1.7-1.6-2l-24.8-8.2c-0.9-0.3-1.8-0.1-2.5,0.6c-0.6,0.6-0.9,1.6-0.6,2.5l8.2,24.8c0.3,0.9,1.1,1.5,2,1.6c0.9,0.1,1.8-0.3,2.3-1L353.1,513.5z" />
212
+ </g>
213
+ </svg>
214
+ );
215
+ }
216
+
217
+ // ─────────────────────────────────────────────────────────────────────
218
+ // Lookup map
219
+ // ─────────────────────────────────────────────────────────────────────
220
+ export const BLOCK_CARD_ICONS: Record<string, React.FC> = {
221
+ textBlock: TextBlockCardIcon,
222
+ imageBlock: ImageBlockCardIcon,
223
+ imageGridBlock: ImageGridBlockCardIcon,
224
+ videoBlock: VideoBlockCardIcon,
225
+ spacerBlock: SpacerBlockCardIcon,
226
+ buttonBlock: ButtonBlockCardIcon,
227
+ };
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import { BLOCK_TYPE_REGISTRY } from "../../lib/builder/types";
5
5
  import type { BlockType, BlockTypeInfo } from "../../lib/builder/types";
6
- import { BLOCK_GRADIENTS, BLOCK_ICON_COMPONENTS } from "./blockStyles";
6
+ import { BLOCK_CARD_ICONS } from "./BlockCardIcons";
7
7
 
8
8
  interface BlockTypePickerProps {
9
9
  onSelect: (type: BlockType) => void;
@@ -35,68 +35,48 @@ function BlockCard({
35
35
  onHover: () => void;
36
36
  onLeave: () => void;
37
37
  }) {
38
- const cardGradient = BLOCK_GRADIENTS[block.type];
39
38
  const labels = BLOCK_LABELS[block.type];
40
- const IconComponent = BLOCK_ICON_COMPONENTS[block.type];
39
+ const CardIcon = BLOCK_CARD_ICONS[block.type];
41
40
 
42
41
  return (
43
42
  <button
44
43
  onClick={onSelect}
45
44
  onMouseEnter={onHover}
46
45
  onMouseLeave={onLeave}
47
- className="relative flex items-center gap-3 rounded-2xl px-3.5 py-3 transition-all text-left group overflow-hidden border-0"
46
+ className="relative flex items-center rounded-2xl text-left group overflow-hidden border-0 h-[96px]"
48
47
  style={{
49
- background: cardGradient || "#f5f5f5",
50
- transform: isHovered ? "translateY(-1px) scale(1.015)" : "translateY(0) scale(1)",
51
- boxShadow: isHovered
52
- ? "0 8px 24px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.3)"
53
- : "0 2px 8px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.2)",
54
- transition: "all 0.3s cubic-bezier(0.23, 1, 0.32, 1)",
48
+ background: "#f4f4f4",
49
+ transform: isHovered ? "translateY(-1px)" : "translateY(0)",
50
+ transition: "transform 200ms cubic-bezier(0.23, 1, 0.32, 1)",
55
51
  }}
56
52
  >
57
- {/* Glass overlay */}
58
- <div
59
- className="absolute inset-0 rounded-2xl pointer-events-none"
60
- style={{
61
- background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)",
62
- }}
63
- />
64
-
65
- {/* Icon container — frosted glass */}
66
- <div
67
- className="relative shrink-0 flex items-center justify-center"
68
- style={{
69
- width: 44,
70
- height: 44,
71
- borderRadius: 12,
72
- background: "rgba(255,255,255,0.4)",
73
- backdropFilter: "blur(8px)",
74
- boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
75
- transition: "transform 0.3s",
76
- transform: isHovered ? "scale(1.08)" : "scale(1)",
77
- }}
78
- >
79
- {IconComponent ? <IconComponent /> : null}
53
+ {/* Icon artwork — full-bleed on the left. Width = height × 220/120
54
+ so the SVG's viewBox fills the container exactly (no sub-pixel seam). */}
55
+ <div className="shrink-0 h-full" style={{ width: 176 }}>
56
+ {CardIcon ? <CardIcon /> : null}
80
57
  </div>
81
58
 
82
59
  {/* Text */}
83
- <div className="relative z-10 min-w-0">
84
- <p
85
- className="text-sm font-semibold truncate"
86
- style={{
87
- color: "rgba(0,0,0,0.72)",
88
- textShadow: "0 1px 0 rgba(255,255,255,0.3)",
89
- }}
90
- >
60
+ <div className="min-w-0 pr-5 py-4">
61
+ <p className="text-[17px] font-semibold text-[#2b2f38] truncate leading-tight">
91
62
  {labels?.label || block.label}
92
63
  </p>
93
- <p
94
- className="text-xs truncate leading-snug mt-0.5"
95
- style={{ color: "rgba(0,0,0,0.42)" }}
96
- >
64
+ <p className="text-[13px] text-[#9096a0] truncate leading-snug mt-1">
97
65
  {labels?.description || block.description}
98
66
  </p>
99
67
  </div>
68
+
69
+ {/* Hover stroke overlay — absolute so it renders ON TOP of the SVG icon;
70
+ inset box-shadow painted on the card's edge would otherwise be covered
71
+ by the SVG's own background fill in the icon half of the card. */}
72
+ <span
73
+ aria-hidden="true"
74
+ className="absolute inset-0 rounded-2xl pointer-events-none"
75
+ style={{
76
+ boxShadow: isHovered ? "inset 0 0 0 2px #4794E2" : "inset 0 0 0 2px transparent",
77
+ transition: "box-shadow 160ms ease",
78
+ }}
79
+ />
100
80
  </button>
101
81
  );
102
82
  }
@@ -117,7 +97,7 @@ export default function BlockTypePicker({
117
97
  onClick={onClose}
118
98
  >
119
99
  <div
120
- className="w-full max-w-2xl rounded-2xl bg-white max-h-[80vh] flex flex-col shadow-2xl border border-neutral-200/50 overflow-hidden"
100
+ className="w-full max-w-4xl rounded-2xl bg-white max-h-[80vh] flex flex-col shadow-2xl border border-neutral-200/50 overflow-hidden"
121
101
  style={{ fontFamily: "Inter, system-ui, sans-serif" }}
122
102
  onClick={(e) => e.stopPropagation()}
123
103
  >
@@ -127,23 +107,16 @@ export default function BlockTypePicker({
127
107
  <h3 className="text-lg font-semibold text-neutral-900">
128
108
  Add Block
129
109
  </h3>
130
- <div className="flex items-center gap-3">
131
- {insertIndex !== undefined && (
132
- <span className="text-xs text-neutral-400 bg-neutral-100 px-3 py-1 rounded-full font-medium">
133
- Position: {insertIndex + 1}
134
- </span>
135
- )}
136
- <button
137
- onClick={onClose}
138
- className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 transition-colors"
139
- aria-label="Close block picker"
140
- >
141
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
142
- <line x1="18" y1="6" x2="6" y2="18" />
143
- <line x1="6" y1="6" x2="18" y2="18" />
144
- </svg>
145
- </button>
146
- </div>
110
+ <button
111
+ onClick={onClose}
112
+ className="w-8 h-8 rounded-lg flex items-center justify-center text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 transition-colors"
113
+ aria-label="Close block picker"
114
+ >
115
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
116
+ <line x1="18" y1="6" x2="6" y2="18" />
117
+ <line x1="6" y1="6" x2="18" y2="18" />
118
+ </svg>
119
+ </button>
147
120
  </div>
148
121
  <p className="text-sm text-neutral-400">
149
122
  Choose a block and add it to your page
@@ -22,8 +22,12 @@ import { getSiteConfig } from "../../lib/config";
22
22
  // double-click zoom, cursor changes, sessionStorage
23
23
  // ============================================
24
24
 
25
- /** Gap between device frames in canvas pixels */
26
- const FRAME_GAP = 80;
25
+ /** Gap between device frames in canvas pixels.
26
+ * Needs breathing room for the floating section pill (rendered by SortableRow)
27
+ * which sits ~98px left of its row at 1× zoom, and up to ~143px at low zoom
28
+ * due to the counter-scale cap of 1.5×. 140 clears it without making the
29
+ * canvas feel sparse. */
30
+ const FRAME_GAP = 140;
27
31
 
28
32
  /** Device order for horizontal layout */
29
33
  const DEVICE_ORDER: DeviceViewport[] = ["desktop", "tablet", "phone"];
@@ -44,12 +44,12 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
44
44
  style={{
45
45
  width: 180,
46
46
  minHeight: 80,
47
- background: "rgba(7, 107, 255, 0.08)",
47
+ background: "rgba(71, 148, 226, 0.08)",
48
48
  backdropFilter: "blur(8px)",
49
49
  opacity: 0.85,
50
50
  borderRadius: 8,
51
51
  border: `2px solid ${BUILDER_BLUE}`,
52
- boxShadow: "0 8px 32px rgba(7, 107, 255, 0.3)",
52
+ boxShadow: "0 8px 32px rgba(71, 148, 226, 0.3)",
53
53
  }}
54
54
  >
55
55
  {/* Column header badge */}
@@ -59,7 +59,7 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
59
59
  alignItems: "center",
60
60
  gap: 8,
61
61
  padding: "8px 12px",
62
- borderBottom: "1px solid rgba(7, 107, 255, 0.2)",
62
+ borderBottom: "1px solid rgba(71, 148, 226, 0.2)",
63
63
  }}
64
64
  >
65
65
  <svg width="10" height="10" viewBox="0 0 10 10" fill={BUILDER_BLUE}>
@@ -8,9 +8,12 @@ import { useBuilderStore } from "../../lib/builder/store";
8
8
  *
9
9
  * 3-state visual matching the column resize handles in SectionV2Column:
10
10
  * - Idle: invisible (transparent hit area only)
11
- * - Hover: teal pill line appears
11
+ * - Hover: violet pill line appears
12
12
  * - Dragging: full-width line with glow + center dot
13
13
  *
14
+ * Uses the section accent (#7500d5) to match the Cover Section's side pill
15
+ * and outline — same semantic colour system as Parallax / Custom sections.
16
+ *
14
17
  * Session 176: Cover Sections — Phase 5.
15
18
  */
16
19
 
@@ -23,7 +26,7 @@ interface CoverRowResizeHandleProps {
23
26
  isSectionHovered: boolean;
24
27
  }
25
28
 
26
- const HANDLE_COLOR = "#0d9488";
29
+ const HANDLE_COLOR = "#7500d5";
27
30
 
28
31
  export default function CoverRowResizeHandle({
29
32
  sectionKey,
@@ -8,6 +8,7 @@ import SectionV2Canvas from "./SectionV2Canvas";
8
8
  import CoverRowResizeHandle from "./CoverRowResizeHandle";
9
9
  import { DEVICE_HEIGHTS } from "../../lib/builder/types";
10
10
  import { adminAssetUrl } from "../../lib/assets";
11
+ import { normalizeRowHeights } from "../../lib/builder/store-cover";
11
12
 
12
13
  /**
13
14
  * CoverSectionCanvas — renders a CoverSection in the builder canvas.
@@ -35,13 +36,24 @@ function getEffectiveCoverRows(section: CoverSection, viewport: DeviceViewport):
35
36
  const vp = viewport as "tablet" | "phone";
36
37
  const overrides = section.responsive?.[vp]?.cover_rows;
37
38
  if (!overrides || overrides.length === 0) return section.cover_rows;
38
- return section.cover_rows.map((row) => {
39
+
40
+ // Partial overrides (only some rows have a tablet/phone value) produce a
41
+ // merged array whose sum is not necessarily 100 — desktop values for the
42
+ // non-overridden rows don't know about the tablet overrides. Normalize the
43
+ // final set so the CSS `grid-template-rows` stays valid.
44
+ const merged = section.cover_rows.map((row) => {
39
45
  const override = overrides.find((o) => o._key === row._key);
40
46
  if (override?.height_percent !== undefined) {
41
47
  return { ...row, height_percent: override.height_percent };
42
48
  }
43
49
  return row;
44
50
  });
51
+
52
+ const total = merged.reduce((acc, r) => acc + r.height_percent, 0);
53
+ if (Math.abs(total - 100) <= 0.5) return merged;
54
+
55
+ const normalized = normalizeRowHeights(merged.map((r) => r.height_percent));
56
+ return merged.map((r, i) => ({ ...r, height_percent: normalized[i] ?? r.height_percent }));
45
57
  }
46
58
 
47
59
  export default function CoverSectionCanvas({
@@ -102,37 +114,9 @@ export default function CoverSectionCanvas({
102
114
  onMouseEnter={() => setIsSectionHovered(true)}
103
115
  onMouseLeave={() => setIsSectionHovered(false)}
104
116
  >
105
- {/* Header bar */}
106
- <div
107
- className="flex items-center gap-2 px-3 py-2 cursor-pointer"
108
- style={{
109
- background: store.selectedRowKey === section._key
110
- ? `linear-gradient(135deg, #ccfbf1 0%, #b2f5ea 100%)`
111
- : `linear-gradient(135deg, #f0fdfa 0%, #e6fffa 100%)`,
112
- borderBottom: `1px solid ${COVER_ACCENT}25`,
113
- borderRadius: "12px 12px 0 0",
114
- }}
115
- onClick={(e) => {
116
- e.stopPropagation();
117
- store.selectRow(section._key);
118
- }}
119
- >
120
- <span className="text-[11px] font-semibold" style={{ color: COVER_ACCENT }}>
121
- ◆ Cover Section
122
- </span>
123
- <span
124
- className="inline-flex items-center justify-center rounded-full text-[9px] font-bold text-white min-w-[18px] h-[18px] px-1"
125
- style={{ background: COVER_ACCENT }}
126
- >
127
- {section.cover_rows.length}
128
- </span>
129
- <div className="flex-1" />
130
- <span className="text-[9px] text-neutral-400 uppercase tracking-wider">
131
- {section.height}
132
- </span>
133
- </div>
134
-
135
- {/* Cover container — simulated viewport height */}
117
+ {/* Cover container — simulated viewport height. The previous top
118
+ header bar was removed; row settings now live in the section pill
119
+ (see SortableRow). */}
136
120
  <div
137
121
  className="relative"
138
122
  style={{ height: containerHeight }}
@@ -183,10 +167,28 @@ export default function CoverSectionCanvas({
183
167
  />
184
168
  )}
185
169
 
186
- {/* Proportional rows */}
170
+ {/* Proportional rows.
171
+ *
172
+ * IMPORTANT — do NOT add `zIndex`, `isolate`, `transform`, `filter`,
173
+ * or any other property that establishes a new CSS stacking context
174
+ * on this wrapper. The column chrome (drag handle, delete button)
175
+ * inside each SectionV2Canvas/SortableColumn is absolutely positioned
176
+ * at z-[6] and must stack ABOVE the side pill (z-[5]) rendered by
177
+ * the parent SortableRow. If this wrapper creates a stacking context,
178
+ * the chrome gets trapped below the pill regardless of its local
179
+ * z-index — the whole subtree ends up at this wrapper's level in the
180
+ * parent stacking context.
181
+ *
182
+ * The rows visually stack above the absolute background/overlay divs
183
+ * naturally because they come LATER in the DOM and those bg divs
184
+ * don't have a positive z-index either.
185
+ *
186
+ * See `lib/builder/__tests__/section-visibility.test.ts` for the
187
+ * related nav-colour logic and this file's git history for the
188
+ * original bug (Session 178+: column chrome clipped in Cover).
189
+ */}
187
190
  <div
188
191
  className="relative flex flex-col h-full"
189
- style={{ zIndex: 1 }}
190
192
  >
191
193
  {virtualSectionsPerRow.map(({ row, rowNumber, virtualSection }, rowIndex) => {
192
194
  const alignMap = { start: "flex-start", center: "center", end: "flex-end" };
@@ -205,22 +207,13 @@ export default function CoverSectionCanvas({
205
207
  justifyContent: alignMap[row.vertical_align] || "flex-start",
206
208
  }}
207
209
  >
208
- {/* Row label */}
209
- <div
210
- className="absolute top-1 right-2 pointer-events-none z-10"
211
- style={{
212
- fontSize: 9,
213
- color: `${COVER_ACCENT}99`,
214
- fontWeight: 600,
215
- }}
216
- >
217
- Row {rowNumber} · {row.height_percent}%
218
- {row.vertical_align !== "start" && ` · ${row.vertical_align}`}
219
- </div>
220
-
221
210
  {hasColumns ? (
222
- /* V2 grid for this row's columns — overflow hidden clips content to row bounds */
223
- <div className="flex-1 min-h-0 flex flex-col" style={{ overflow: "hidden" }}>
211
+ /* V2 grid for this row's columns — overflow visible so the
212
+ column chrome (drag handle, delete button) that translates
213
+ outside column bounds doesn't get clipped. The public
214
+ renderer still clips at the <section> level so overflowing
215
+ content never leaves the cover on the live site. */
216
+ <div className="flex-1 min-h-0 flex flex-col" style={{ overflow: "visible" }}>
224
217
  <SectionV2Canvas
225
218
  section={virtualSection}
226
219
  onAddBlockTarget={onAddBlockTarget}
@@ -247,9 +240,9 @@ export default function CoverSectionCanvas({
247
240
  className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
248
241
  style={{
249
242
  padding: "5px 16px",
250
- background: `rgba(7, 107, 255, 0.10)`,
251
- color: "#076bff",
252
- border: "1px dashed rgba(7, 107, 255, 0.4)",
243
+ background: `rgba(71, 148, 226, 0.10)`,
244
+ color: "#4794e2",
245
+ border: "1px dashed rgba(71, 148, 226, 0.4)",
253
246
  opacity: isSectionHovered ? 1 : 0,
254
247
  pointerEvents: isSectionHovered ? "auto" : "none",
255
248
  transition: "opacity 150ms",
@@ -137,7 +137,7 @@ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: {
137
137
  if (!block) return null;
138
138
  const info = ALL_BLOCK_INFO.find((b) => b.type === block!._type);
139
139
  return (
140
- <div className="rounded border border-[#0d9668] bg-[#0d9668]/10 px-3 py-2 shadow-lg shadow-[#0d9668]/20 backdrop-blur-sm">
140
+ <div className="rounded border border-[#4794e2] bg-[#4794e2]/10 px-3 py-2 shadow-lg shadow-[#4794e2]/20 backdrop-blur-sm">
141
141
  <div className="flex items-center gap-2">
142
142
  <span className="text-xs">{info?.icon || "▪"}</span>
143
143
  <span className="text-xs text-white">{info?.label || block._type}</span>
@@ -165,7 +165,7 @@ export const InsertionLines = memo(function InsertionLines({
165
165
  borderRadius: "50%",
166
166
  background: BUILDER_BLUE,
167
167
  border: "2px solid white",
168
- boxShadow: "0 1px 4px rgba(7, 107, 255, 0.4)",
168
+ boxShadow: "0 1px 4px rgba(71, 148, 226, 0.4)",
169
169
  display: "flex",
170
170
  alignItems: "center",
171
171
  justifyContent: "center",