@morphika/andami 0.5.1 → 0.5.3

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 (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. package/styles/base.css +114 -114
@@ -4,6 +4,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
4
4
  import { useBuilderStore } from "../../lib/builder/store";
5
5
  import { getCsrfToken } from "../../lib/csrf-client";
6
6
  import { ADMIN_ACCENT } from "../../lib/builder/constants";
7
+ import { BubbleTooltip } from "./BubbleIcons";
7
8
 
8
9
  // ============================================
9
10
  // SectionEditorBar — Top bar for custom section editor mode
@@ -134,10 +135,11 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
134
135
  <span className="text-[#444]">/</span>
135
136
  <button
136
137
  onClick={handleCancel}
137
- className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
138
- title={`Back to ${pageTitle || "page"}`}
138
+ className="group/bb relative text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
139
+ aria-label={`Back to ${pageTitle || "page"}`}
139
140
  >
140
141
  {pageTitle || "Page"}
142
+ <BubbleTooltip>{`Back to ${pageTitle || "page"}`}</BubbleTooltip>
141
143
  </button>
142
144
  <span className="text-[#444]">/</span>
143
145
  <span className="text-[#999] font-medium">
@@ -155,7 +157,7 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
155
157
  value={name}
156
158
  onChange={(e) => setName(e.target.value)}
157
159
  placeholder="Section name..."
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"
160
+ className="bg-[#2a2a2a] text-white text-sm px-3 py-1.5 rounded border border-[#3a3a3a] focus:border-[#3580f9] focus:outline-none w-64 transition-colors text-center"
159
161
  />
160
162
  </div>
161
163
 
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect } from "react";
4
- import { SECTION_TYPE_REGISTRY } from "../../lib/builder/types";
4
+ import { SECTION_TYPE_REGISTRY, type SectionBlockType } from "../../lib/builder/types";
5
5
  import type { CustomSectionListItem } from "../../lib/sanity/types";
6
6
  import { SECTION_CARD_ICONS } from "./SectionCardIcons";
7
+ import { BubbleTooltip } from "./BubbleIcons";
7
8
 
8
9
  // ── V2 layout presets (use cascade preset names) ──
9
10
  type V2Preset = "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3";
@@ -83,7 +84,7 @@ function SectionCard({
83
84
 
84
85
  interface SectionTypePickerProps {
85
86
  onSelectEmptyV2?: (preset: "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3") => void;
86
- onSelectSection: (blockType: "projectGridBlock" | "projectCarouselBlock") => void;
87
+ onSelectSection: (blockType: SectionBlockType) => void;
87
88
  onSelectParallaxGroup?: () => void;
88
89
  onSelectCoverSection?: () => void;
89
90
  onSelectCustomSection?: (section: CustomSectionListItem) => void;
@@ -212,14 +213,14 @@ export default function SectionTypePicker({
212
213
  }
213
214
  onClose();
214
215
  }}
215
- className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#4794e2] hover:bg-[#4794e2]/5 transition-colors group shadow-sm"
216
- title={label}
216
+ className="group/bb relative rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#3580f9] hover:bg-[#3580f9]/5 transition-colors group shadow-sm"
217
+ aria-label={label}
217
218
  >
218
219
  <div className="flex gap-1 h-6">
219
220
  {widths.map((w, i) => (
220
221
  <div
221
222
  key={i}
222
- className="bg-neutral-200 group-hover:bg-[#4794e2]/30 rounded-sm transition-colors"
223
+ className="bg-neutral-200 group-hover:bg-[#3580f9]/30 rounded-sm transition-colors"
223
224
  style={{ flex: w }}
224
225
  />
225
226
  ))}
@@ -227,6 +228,7 @@ export default function SectionTypePicker({
227
228
  <p className="text-xs text-neutral-500 mt-1.5 group-hover:text-neutral-700">
228
229
  {label}
229
230
  </p>
231
+ <BubbleTooltip>{label}</BubbleTooltip>
230
232
  </button>
231
233
  ))}
232
234
  </div>
@@ -310,7 +310,7 @@ export default function SectionV2Canvas({
310
310
  : showAsDropTarget
311
311
  ? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
312
312
  : isSectionHovered
313
- ? "border-[#4794e2]/25 text-[#4794e2]/50 hover:text-[#4794e2] hover:border-[#4794e2]/60 hover:bg-[#4794e2]/5 opacity-100"
313
+ ? "border-[#3580f9]/25 text-[#3580f9]/50 hover:text-[#3580f9] hover:border-[#3580f9]/60 hover:bg-[#3580f9]/5 opacity-100"
314
314
  : "border-transparent text-transparent opacity-0 pointer-events-none"
315
315
  }`}
316
316
  >
@@ -9,9 +9,14 @@ import {
9
9
  import { useBuilderStore } from "../../lib/builder/store";
10
10
  import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
11
11
  import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
12
- import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
12
+ import {
13
+ getColumnVerticalAlign,
14
+ getBackgroundStyles,
15
+ getBorderStyles,
16
+ } from "../../lib/builder/layout-styles";
13
17
  import { isSectionBlockType } from "../../lib/builder/types";
14
18
  import { BUILDER_BLUE } from "../../lib/builder/constants";
19
+ import { BubbleTooltip, CloseIcon, DragDropIcon } from "./BubbleIcons";
15
20
 
16
21
  // ============================================
17
22
  // SectionV2Column — Individual column in a V2 section grid
@@ -84,17 +89,17 @@ function ResizeHandle({
84
89
  height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
85
90
  borderRadius: isActive ? "50%" : 999,
86
91
  backgroundColor: isActive
87
- ? "rgba(71, 148, 226, 0.9)"
92
+ ? "rgba(53, 128, 249, 0.9)"
88
93
  : isHoveredEdge
89
- ? "rgba(71, 148, 226, 0.7)"
94
+ ? "rgba(53, 128, 249, 0.7)"
90
95
  : showChrome
91
- ? "rgba(71, 148, 226, 0.5)"
92
- : "rgba(71, 148, 226, 0.2)",
96
+ ? "rgba(53, 128, 249, 0.5)"
97
+ : "rgba(53, 128, 249, 0.2)",
93
98
  transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
94
99
  boxShadow: isActive
95
- ? "0 0 10px rgba(71, 148, 226, 0.5)"
100
+ ? "0 0 10px rgba(53, 128, 249, 0.5)"
96
101
  : isHoveredEdge
97
- ? "0 0 6px rgba(71, 148, 226, 0.2)"
102
+ ? "0 0 6px rgba(53, 128, 249, 0.2)"
98
103
  : undefined,
99
104
  }}
100
105
  />
@@ -229,6 +234,12 @@ export default function SectionV2Column({
229
234
  // Column-level vertical alignment from blocks' align_v settings
230
235
  const colJustify = getColumnVerticalAlign(column.blocks || []);
231
236
 
237
+ // Column-level background + border (desktop-only — no responsive overrides).
238
+ const columnLayoutStyles: React.CSSProperties = {
239
+ ...getBackgroundStyles(column),
240
+ ...getBorderStyles(column),
241
+ };
242
+
232
243
  // ---- Preview mode ----
233
244
  if (previewMode) {
234
245
  return (
@@ -243,6 +254,7 @@ export default function SectionV2Column({
243
254
  ...(colJustify ? { justifyContent: colJustify } : {}),
244
255
  height: "100%",
245
256
  minHeight: 0,
257
+ ...columnLayoutStyles,
246
258
  }}
247
259
  >
248
260
  <SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
@@ -279,6 +291,7 @@ export default function SectionV2Column({
279
291
  transition: isDraggedColumn
280
292
  ? "none"
281
293
  : "opacity 150ms, box-shadow 150ms, transform 150ms ease-out",
294
+ ...columnLayoutStyles,
282
295
  }}
283
296
  ref={setBlockDropRef}
284
297
  onClick={handleClick}
@@ -289,17 +302,17 @@ export default function SectionV2Column({
289
302
  <div
290
303
  className="pointer-events-none absolute inset-0 z-[1] rounded"
291
304
  style={{
292
- transition: "box-shadow 150ms, border 150ms",
305
+ transition: "box-shadow 150ms",
293
306
  ...(isSwapTarget
294
- ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(71, 148, 226, 0.08)" }
307
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(53, 128, 249, 0.08)" }
295
308
  : isBlockOver
296
309
  ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
297
310
  : isSelected
298
- ? { boxShadow: `inset 0 0 0 2px rgba(71, 148, 226, 0.6)` }
311
+ ? { boxShadow: `inset 0 0 0 2px rgba(53, 128, 249, 0.6)` }
299
312
  : isHovered
300
- ? { boxShadow: `inset 0 0 0 1.5px rgba(71, 148, 226, 0.5)` }
313
+ ? { boxShadow: `inset 0 0 0 1.5px rgba(53, 128, 249, 0.5)` }
301
314
  : showFaintOutline
302
- ? { border: `1px dashed rgba(71, 148, 226, 0.2)`, borderRadius: 4 }
315
+ ? { boxShadow: `inset 0 0 0 1px rgba(53, 128, 249, 0.2)`, borderRadius: 4 }
303
316
  : undefined),
304
317
  }}
305
318
  />
@@ -354,64 +367,65 @@ export default function SectionV2Column({
354
367
  </>
355
368
  )}
356
369
 
357
- {/* Delete buttonred circle top right, positioned outside the column box.
370
+ {/* Column pillsingle horizontal pill sitting OUTSIDE the column top-left edge,
371
+ with a 2px gap so its border never touches the column outline. Structure:
372
+ [drag-drop] | Col | [X]. Aligns horizontally with the block pill when the
373
+ first block in the column has no vertical offset.
358
374
  Nested pattern: outer div positions, inner div counter-scales. */}
359
375
  <div
360
- className={`absolute z-[6] transition-opacity ${
376
+ className={`absolute top-0 left-0 z-[6] transition-opacity ${
361
377
  showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
362
378
  }`}
363
- style={{
364
- top: 0,
365
- right: 0,
366
- transform: "translate(40%, -40%)",
367
- }}
379
+ style={{ transform: "translateY(calc(-100% - 2px))" }}
368
380
  onClick={(e) => e.stopPropagation()}
369
381
  >
370
- <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
371
- <button
372
- onClick={handleDelete}
373
- className="w-5 h-5 rounded-full text-white flex items-center justify-center transition-transform hover:scale-[1.15]"
374
- style={{ background: "#ef4848" }}
375
- title="Delete column"
376
- aria-label="Delete column"
377
- >
378
- <svg width="10" height="10" viewBox="0 0 10 10">
379
- <path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
380
- </svg>
381
- </button>
382
- </div>
383
- </div>
384
-
385
- {/* Drag grip — top left corner, uses @dnd-kit useDraggable.
386
- Nested pattern: outer div positions, inner div counter-scales. */}
387
- <div
388
- className={`absolute z-[6] transition-opacity ${
389
- showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
390
- }`}
391
- style={{
392
- top: 0,
393
- left: 0,
394
- transform: "translate(-30%, -30%)",
395
- }}
396
- >
397
- <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
398
- <div
399
- className="w-5 h-5 rounded-full bg-[#4794e2] text-white flex items-center justify-center shadow-md cursor-grab active:cursor-grabbing transition-transform hover:scale-[1.15] hover:bg-[#3578b8] hover:shadow-blue-500/30 hover:shadow-lg"
400
- title="Drag to move column"
401
- aria-label="Move column"
402
- onMouseDown={(e) => {
403
- e.stopPropagation();
404
- onStartDrag?.(e);
405
- }}
406
- onClick={(e) => {
407
- e.stopPropagation();
408
- onSelect();
409
- }}
410
- >
411
- <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
412
- <path d="M8 0l2.5 3h-2v4.5H13v-2L16 8l-3 2.5v-2H8.5V13h2L8 16l-2.5-3h2V8.5H3v2L0 8l3-2.5v2h4.5V3h-2L8 0z" />
413
- </svg>
414
- </div>
382
+ <div style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "bottom left" }}>
383
+ <div
384
+ className="flex items-center rounded-[5px]"
385
+ style={{
386
+ background: "#c0d7ff",
387
+ border: "1.5px solid #3580f9",
388
+ }}
389
+ >
390
+ {/* Drag handle starts a column drag via @dnd-kit, selects on click */}
391
+ <button
392
+ type="button"
393
+ className="group/bb relative text-[#3580f9] hover:bg-[#3580f9]/15 transition-colors px-1.5 py-1 flex items-center justify-center cursor-grab active:cursor-grabbing rounded-l-[3px]"
394
+ aria-label="Move column"
395
+ onMouseDown={(e) => {
396
+ e.stopPropagation();
397
+ onStartDrag?.(e);
398
+ }}
399
+ onClick={(e) => {
400
+ e.stopPropagation();
401
+ onSelect();
402
+ }}
403
+ >
404
+ <DragDropIcon size={14} />
405
+ <BubbleTooltip>Drag to move</BubbleTooltip>
406
+ </button>
407
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
408
+ {/* Label — clicking selects the column */}
409
+ <button
410
+ type="button"
411
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
412
+ className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#3580f9]/15 transition-colors"
413
+ style={{ color: "#3580f9" }}
414
+ aria-label="Select column"
415
+ >
416
+ Col
417
+ </button>
418
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
419
+ {/* Delete */}
420
+ <button
421
+ onClick={handleDelete}
422
+ className="group/bb relative text-[#3580f9] hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-1 flex items-center justify-center rounded-r-[3px]"
423
+ aria-label="Delete column"
424
+ >
425
+ <CloseIcon size={14} />
426
+ <BubbleTooltip>Delete</BubbleTooltip>
427
+ </button>
428
+ </div>
415
429
  </div>
416
430
  </div>
417
431
 
@@ -473,8 +487,8 @@ export default function SectionV2Column({
473
487
  padding: "5px 16px",
474
488
  pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
475
489
  background: "#d2e3ff",
476
- color: "#4794e2",
477
- border: "1px dashed #4794e2",
490
+ color: "#3580f9",
491
+ border: "1.5px dashed #3580f9",
478
492
  }}
479
493
  >
480
494
  + Add Block
@@ -502,8 +516,8 @@ export default function SectionV2Column({
502
516
  padding: "4px 14px",
503
517
  pointerEvents: showChrome ? "auto" : "none",
504
518
  background: "#d2e3ff",
505
- color: "#4794e2",
506
- border: "1px dashed #4794e2",
519
+ color: "#3580f9",
520
+ border: "1.5px dashed #3580f9",
507
521
  }}
508
522
  >
509
523
  + Add Block
@@ -25,6 +25,7 @@ import { useBuilderStore } from "../../lib/builder/store";
25
25
  import { useSettingsPanelSelection } from "./settings-panel/useSettingsPanelSelection";
26
26
  import { AnimationTab } from "./settings-panel/AnimationTab";
27
27
  import { ColumnV2AnimationTab } from "./settings-panel/ColumnV2AnimationTab";
28
+ import { ColumnV2LayoutTab } from "./settings-panel/ColumnV2LayoutTab";
28
29
  import { CustomSectionSettings } from "./settings-panel/CustomSectionSettings";
29
30
  import {
30
31
  BlockLayoutTab,
@@ -40,6 +41,7 @@ import {
40
41
  CoverSectionSettings,
41
42
  } from "./settings-panel";
42
43
  import CoverSectionLayoutTab from "./settings-panel/CoverSectionLayoutTab";
44
+ import { BubbleTooltip } from "./BubbleIcons";
43
45
 
44
46
  type SettingsTab = "settings" | "layout" | "seo" | "animation";
45
47
 
@@ -75,11 +77,11 @@ export default function SettingsPanel() {
75
77
  }
76
78
  }, [selectionKey]);
77
79
 
78
- // Columns have Settings + Animation — fall back if Layout or SEO tab was active
80
+ // Columns have Settings + Layout + Animation — fall back if SEO tab was active
79
81
  // Parallax group header has only Settings — fall back if Layout/SEO or Animation tab was active
80
82
  // Page level has Settings + SEO + Animation — fall back if Layout tab was active
81
83
  useEffect(() => {
82
- if (isColumnOnly && (activeTab === "layout" || activeTab === "seo")) {
84
+ if (isColumnOnly && activeTab === "seo") {
83
85
  setActiveTab("settings");
84
86
  }
85
87
  if (isParallaxGroupOnly && (activeTab === "layout" || activeTab === "seo" || activeTab === "animation")) {
@@ -134,13 +136,14 @@ export default function SettingsPanel() {
134
136
  return (
135
137
  <button
136
138
  onClick={onDelete}
137
- className="p-1.5 rounded-md hover:bg-red-500/20 transition-colors group"
138
- title={deleteTitle}
139
+ className="group/bb relative p-1.5 rounded-md hover:bg-red-500/20 transition-colors"
140
+ aria-label={deleteTitle}
139
141
  >
140
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-black/40 group-hover:text-[var(--admin-error)] transition-colors">
142
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-black/40 group-hover/bb:text-[var(--admin-error)] transition-colors">
141
143
  <polyline points="3 6 5 6 21 6" />
142
144
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
143
145
  </svg>
146
+ <BubbleTooltip>{deleteTitle}</BubbleTooltip>
144
147
  </button>
145
148
  );
146
149
  })()}
@@ -204,11 +207,6 @@ export default function SettingsPanel() {
204
207
  });
205
208
  }
206
209
  }
207
- // Columns: remove Layout tab (keep Settings + Animation)
208
- if (isColumnOnly) {
209
- const layoutIdx = tabs.findIndex((t) => t.id === "layout");
210
- if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
211
- }
212
210
  // Parallax group header: only Settings (no Layout, no Animation)
213
211
  if (isParallaxGroupOnly) {
214
212
  const layoutIdx = tabs.findIndex((t) => t.id === "layout");
@@ -268,9 +266,11 @@ export default function SettingsPanel() {
268
266
  {selectedParallaxGroup && !selectedParallaxSlide && !selectedBlock ? (
269
267
  <ParallaxGroupSettings group={selectedParallaxGroup} />
270
268
  ) : selectedParallaxSlide && selectedColumnV2 && !selectedBlock ? (
271
- // Column inside a parallax slide — Settings or Animation tab
269
+ // Column inside a parallax slide — Settings / Layout / Animation
272
270
  activeTab === "animation" ? (
273
271
  <ColumnV2AnimationTab section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
272
+ ) : activeTab === "layout" ? (
273
+ <ColumnV2LayoutTab section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
274
274
  ) : (
275
275
  <ColumnV2Settings section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
276
276
  )
@@ -320,15 +320,19 @@ export default function SettingsPanel() {
320
320
  <CoverSectionSettings section={selectedCoverSection} />
321
321
  )
322
322
  ) :
323
- /* ---- V2 Section / Column / Block routing ---- */
323
+ /* ---- V2 / Cover Section / Column / Block routing ---- */
324
324
  /* BUG-V2-003 fix: When a block inside a V2 column is selected, show BlockSettings
325
- instead of ColumnV2Settings. Block selection takes priority over column. */
326
- selectedColumnV2 && selectedSectionV2 && !selectedBlock ? (
327
- // V2 Column selected (no block) Settings or Animation tab
325
+ instead of ColumnV2Settings. Block selection takes priority over column.
326
+ Cover-column fix: use effectiveSectionV2 so columns inside a cover section
327
+ route to ColumnV2Settings instead of falling through to PageSettings. */
328
+ selectedColumnV2 && effectiveSectionV2 && !selectedBlock ? (
329
+ // V2 Column or Cover Section column selected (no block) — Settings / Layout / Animation
328
330
  activeTab === "animation" ? (
329
- <ColumnV2AnimationTab section={selectedSectionV2} column={selectedColumnV2} />
331
+ <ColumnV2AnimationTab section={effectiveSectionV2} column={selectedColumnV2} />
332
+ ) : activeTab === "layout" ? (
333
+ <ColumnV2LayoutTab section={effectiveSectionV2} column={selectedColumnV2} />
330
334
  ) : (
331
- <ColumnV2Settings section={selectedSectionV2} column={selectedColumnV2} />
335
+ <ColumnV2Settings section={effectiveSectionV2} column={selectedColumnV2} />
332
336
  )
333
337
  ) : selectedSectionV2 && !selectedBlock ? (
334
338
  // V2 Section selected — route by active tab
@@ -12,6 +12,7 @@ import BlockLivePreview from "./BlockLivePreview";
12
12
  import { getBlockAlignmentStyles, hasBlockAlignment } from "../../lib/builder/layout-styles";
13
13
  import type { BlockLayout } from "../../lib/sanity/types";
14
14
  import { BUILDER_BLOCK } from "../../lib/builder/constants";
15
+ import { ArrowDownIcon, ArrowUpIcon, BubbleTooltip, CloseIcon, CopyIcon } from "./BubbleIcons";
15
16
 
16
17
  interface SortableBlockProps {
17
18
  block: ContentBlock;
@@ -124,7 +125,7 @@ export default function SortableBlock({
124
125
  style={{ ...style, ...(!isFillBlock ? { position: "relative" as const, zIndex: 1 } : {}) }}
125
126
  className={`transition-[opacity,box-shadow] ${
126
127
  isDragging
127
- ? "ring-2 ring-[#4794e2] ring-offset-1 ring-offset-transparent rounded"
128
+ ? "ring-2 ring-[#3580f9] ring-offset-1 ring-offset-transparent rounded"
128
129
  : ""
129
130
  }`}
130
131
  onClick={(e) => {
@@ -145,93 +146,112 @@ export default function SortableBlock({
145
146
  ...(isSelected
146
147
  ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${BUILDER_BLOCK}` }
147
148
  : isHovered
148
- ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(71, 148, 226, 0.4)` }
149
+ ? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(53, 128, 249, 0.4)` }
149
150
  : {}),
150
151
  }}
151
152
  />
152
153
 
153
- {/* Floating toolbar — centered INSIDE top of block, appears on hover or when selected.
154
+ {/* Floating toolbar — top-right of block, sitting OUTSIDE (above) with a 2px gap
155
+ so the pill border never overlaps the column/block outlines.
156
+ When the block has no vertical offset, this pill naturally aligns horizontally
157
+ with the column pill (both hover just above the column's top edge).
154
158
  Positioning (translate) is separated from counter-scaling (scale) into nested
155
- elements so that zoom changes don't shift the anchor point.
156
- Delete button is integrated at the end of the toolbar with a gap separator. */}
159
+ elements so zoom changes don't shift the anchor point. */}
157
160
  <div
158
- className={`absolute top-0 left-1/2 z-[6] transition-opacity ${
161
+ className={`absolute top-0 right-0 z-[6] transition-opacity ${
159
162
  showToolbar ? "opacity-100" : "opacity-0 pointer-events-none"
160
163
  }`}
161
- style={{ transform: "translateX(-50%)", marginTop: 4 }}
164
+ style={{ transform: "translateY(calc(-100% - 2px))" }}
162
165
  onClick={(e) => e.stopPropagation()}
163
166
  >
164
167
  <div
165
- className="flex items-center gap-1.5"
166
- style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "top center" }}
168
+ style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "bottom right" }}
167
169
  >
168
- <div className="flex items-center rounded-[5px] overflow-hidden" style={{
169
- background: "#d2e3ff",
170
- border: "1px solid #4794e2",
171
- }}>
172
- {/* Block type label — first */}
173
- <span className="text-[11px] px-1.5 py-0.5 font-medium" style={{ color: "#4794e2" }}>
174
- {info?.icon || "▪"} {info?.label || block._type}
175
- </span>
176
- {/* Enter animation badge */}
177
- {block.enter_animation?.preset && block.enter_animation.preset !== "none" && (
178
- <span className="text-[10px] text-[#4794e2]/50 px-1 py-0.5 border-l border-[#4794e2]/25" title={`Animation: ${block.enter_animation.preset}`}>
179
-
180
- </span>
181
- )}
182
- {/* Duplicate */}
183
- {onDuplicate && (
170
+ <div
171
+ className="flex items-center rounded-[5px]"
172
+ style={{
173
+ background: "#c0d7ff",
174
+ border: "1.5px solid #3580f9",
175
+ }}
176
+ >
177
+ {/* Block type label — clicking selects the block */}
184
178
  <button
185
- onClick={onDuplicate}
186
- className="text-[#4794e2]/60 hover:text-[#4794e2] transition-colors px-1.5 py-0.5 text-[11px] border-l border-[#4794e2]/25 hover:bg-[#4794e2]/10"
187
- title="Duplicate block (Ctrl+D)"
188
- aria-label="Duplicate block"
179
+ type="button"
180
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
181
+ className="text-[11px] px-2 py-0.5 font-medium rounded-l-[3px] hover:bg-[#3580f9]/15 transition-colors"
182
+ style={{ color: "#3580f9" }}
183
+ aria-label="Select block"
189
184
  >
190
-
185
+ {info?.label || block._type}
191
186
  </button>
192
- )}
193
- {/* Move up arrow */}
194
- <button
195
- onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
196
- className={`transition-colors px-1 py-0.5 text-[11px] border-l border-[#4794e2]/25 ${
197
- canMoveUp
198
- ? "text-[#4794e2]/60 hover:text-[#4794e2] hover:bg-[#4794e2]/10 cursor-pointer"
199
- : "text-[#4794e2]/25 cursor-default"
200
- }`}
201
- title="Move block up"
202
- aria-label="Move block up"
203
- disabled={!canMoveUp}
204
- >
205
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
206
- <path d="M5 2L2 6h6L5 2z" fill="currentColor" />
207
- </svg>
208
- </button>
209
- {/* Move down arrow */}
210
- <button
211
- onClick={() => canMoveDown && reorderBlocks(rowKey, colKey, blockIndex, blockIndex + 1)}
212
- className={`transition-colors px-1 py-0.5 text-[11px] border-l border-[#4794e2]/25 ${
213
- canMoveDown
214
- ? "text-[#4794e2]/60 hover:text-[#4794e2] hover:bg-[#4794e2]/10 cursor-pointer"
215
- : "text-[#4794e2]/25 cursor-default"
216
- }`}
217
- title="Move block down"
218
- aria-label="Move block down"
219
- disabled={!canMoveDown}
220
- >
221
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
222
- <path d="M5 8L2 4h6L5 8z" fill="currentColor" />
223
- </svg>
224
- </button>
225
- {/* Delete — text inside pill, red hover for destructive signal */}
226
- <button
227
- onClick={onDelete}
228
- className="text-[#4794e2]/60 hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-0.5 text-[11px] font-medium border-l border-[#4794e2]/25"
229
- title="Delete block"
230
- aria-label="Delete block"
231
- >
232
- Delete
233
- </button>
234
- </div>
187
+ {/* Enter animation badge */}
188
+ {block.enter_animation?.preset && block.enter_animation.preset !== "none" && (
189
+ <>
190
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
191
+ <span
192
+ className="group/bb relative text-[10px] px-1 py-0.5"
193
+ style={{ color: "#3580f9" }}
194
+ >
195
+
196
+ <BubbleTooltip>{`Animation: ${block.enter_animation.preset}`}</BubbleTooltip>
197
+ </span>
198
+ </>
199
+ )}
200
+ {/* Duplicate */}
201
+ {onDuplicate && (
202
+ <>
203
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
204
+ <button
205
+ onClick={onDuplicate}
206
+ className="group/bb relative text-[#3580f9] hover:bg-[#3580f9]/15 transition-colors px-1.5 py-1 flex items-center justify-center"
207
+ aria-label="Duplicate block"
208
+ >
209
+ <CopyIcon size={14} />
210
+ <BubbleTooltip>Duplicate (⌘D)</BubbleTooltip>
211
+ </button>
212
+ </>
213
+ )}
214
+ {/* Move up */}
215
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
216
+ <button
217
+ onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
218
+ className={`group/bb relative transition-colors px-1.5 py-1 flex items-center justify-center ${
219
+ canMoveUp
220
+ ? "text-[#3580f9] hover:bg-[#3580f9]/15 cursor-pointer"
221
+ : "text-[#3580f9]/40 cursor-default"
222
+ }`}
223
+ aria-label="Move block up"
224
+ disabled={!canMoveUp}
225
+ >
226
+ <ArrowUpIcon size={14} />
227
+ {canMoveUp && <BubbleTooltip>Move up</BubbleTooltip>}
228
+ </button>
229
+ {/* Move down */}
230
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
231
+ <button
232
+ onClick={() => canMoveDown && reorderBlocks(rowKey, colKey, blockIndex, blockIndex + 1)}
233
+ className={`group/bb relative transition-colors px-1.5 py-1 flex items-center justify-center ${
234
+ canMoveDown
235
+ ? "text-[#3580f9] hover:bg-[#3580f9]/15 cursor-pointer"
236
+ : "text-[#3580f9]/40 cursor-default"
237
+ }`}
238
+ aria-label="Move block down"
239
+ disabled={!canMoveDown}
240
+ >
241
+ <ArrowDownIcon size={14} />
242
+ {canMoveDown && <BubbleTooltip>Move down</BubbleTooltip>}
243
+ </button>
244
+ {/* Delete — red hover for destructive cue */}
245
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
246
+ <button
247
+ onClick={onDelete}
248
+ className="group/bb relative text-[#3580f9] hover:text-red-500 hover:bg-red-500/10 transition-colors px-1.5 py-1 flex items-center justify-center rounded-r-[3px]"
249
+ aria-label="Delete block"
250
+ >
251
+ <CloseIcon size={14} />
252
+ <BubbleTooltip>Delete</BubbleTooltip>
253
+ </button>
254
+ </div>
235
255
  </div>
236
256
  </div>
237
257