@morphika/andami 0.5.0 → 0.5.2

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 (122) hide show
  1. package/README.md +151 -36
  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 +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. package/styles/base.css +114 -114
@@ -155,7 +155,7 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
155
155
  value={name}
156
156
  onChange={(e) => setName(e.target.value)}
157
157
  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"
158
+ 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
159
  />
160
160
  </div>
161
161
 
@@ -1,7 +1,7 @@
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
7
 
@@ -83,7 +83,7 @@ function SectionCard({
83
83
 
84
84
  interface SectionTypePickerProps {
85
85
  onSelectEmptyV2?: (preset: "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3") => void;
86
- onSelectSection: (blockType: "projectGridBlock" | "projectCarouselBlock") => void;
86
+ onSelectSection: (blockType: SectionBlockType) => void;
87
87
  onSelectParallaxGroup?: () => void;
88
88
  onSelectCoverSection?: () => void;
89
89
  onSelectCustomSection?: (section: CustomSectionListItem) => void;
@@ -212,14 +212,14 @@ export default function SectionTypePicker({
212
212
  }
213
213
  onClose();
214
214
  }}
215
- className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#4794e2] hover:bg-[#4794e2]/5 transition-colors group shadow-sm"
215
+ className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#3580f9] hover:bg-[#3580f9]/5 transition-colors group shadow-sm"
216
216
  title={label}
217
217
  >
218
218
  <div className="flex gap-1 h-6">
219
219
  {widths.map((w, i) => (
220
220
  <div
221
221
  key={i}
222
- className="bg-neutral-200 group-hover:bg-[#4794e2]/30 rounded-sm transition-colors"
222
+ className="bg-neutral-200 group-hover:bg-[#3580f9]/30 rounded-sm transition-colors"
223
223
  style={{ flex: w }}
224
224
  />
225
225
  ))}
@@ -27,6 +27,13 @@ interface SectionV2CanvasProps {
27
27
  fillHeight?: boolean;
28
28
  /** Offset added to grid_row when adding columns via gaps (used by cover sections where columns are normalized to grid_row 1) */
29
29
  gridRowOffset?: number;
30
+ /**
31
+ * Optional override for the responsive-update action. When provided, used instead of the
32
+ * store's `updateSectionV2Responsive`. Used by CoverSectionCanvas to intercept writes from
33
+ * virtual per-row sections and merge them back into the Cover's flat column list (remapping
34
+ * `grid_row: 1` back to the row's real number and preserving overrides for other rows).
35
+ */
36
+ onUpdateResponsive?: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void;
30
37
  }
31
38
 
32
39
  export default function SectionV2Canvas({
@@ -34,6 +41,7 @@ export default function SectionV2Canvas({
34
41
  onAddBlockTarget,
35
42
  fillHeight,
36
43
  gridRowOffset = 0,
44
+ onUpdateResponsive,
37
45
  }: SectionV2CanvasProps) {
38
46
  const previewMode = useBuilderStore((s) => s.previewMode);
39
47
  const canvasZoom = useBuilderStore((s) => s.canvasZoom);
@@ -44,7 +52,8 @@ export default function SectionV2Canvas({
44
52
  const deleteColumnV2 = useBuilderStore((s) => s.deleteColumnV2);
45
53
  const resizeColumnV2 = useBuilderStore((s) => s.resizeColumnV2);
46
54
  const resizeColumnV2Left = useBuilderStore((s) => s.resizeColumnV2Left);
47
- const updateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
55
+ const storeUpdateSectionV2Responsive = useBuilderStore((s) => s.updateSectionV2Responsive);
56
+ const updateSectionV2Responsive = onUpdateResponsive ?? storeUpdateSectionV2Responsive;
48
57
  const selectColumnV2 = useBuilderStore((s) => s.selectColumnV2);
49
58
  const selectBlock = useBuilderStore((s) => s.selectBlock);
50
59
  const deleteBlock = useBuilderStore((s) => s.deleteBlock);
@@ -153,7 +162,10 @@ export default function SectionV2Canvas({
153
162
  style={{
154
163
  display: "grid",
155
164
  gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
156
- ...(fillHeight ? { gridTemplateRows: "1fr" } : {}),
165
+ // `minmax(0, 1fr)` lets the row shrink below intrinsic content min-size
166
+ // (gaps' minHeight, empty-column wrappers), which is required in Cover
167
+ // sections where the row has a strict proportional height.
168
+ ...(fillHeight ? { gridTemplateRows: "minmax(0, 1fr)" } : {}),
157
169
  columnGap: `${colGap}px`,
158
170
  rowGap: `${rowGap}px`,
159
171
  position: "relative",
@@ -237,6 +249,7 @@ export default function SectionV2Canvas({
237
249
  }
238
250
  onResizeRight={handleResizeRight}
239
251
  onResizeLeft={handleResizeLeft}
252
+ fillHeight={fillHeight}
240
253
  >
241
254
  {(col.blocks || []).map((block, blockIdx) => (
242
255
  <SortableBlock
@@ -286,7 +299,10 @@ export default function SectionV2Canvas({
286
299
  style={{
287
300
  gridColumn: `${gap.grid_column} / span ${gap.span}`,
288
301
  gridRow: gap.grid_row,
289
- minHeight: 70,
302
+ // In fillHeight (Cover), the row has a strict proportional
303
+ // height — don't let the gap force a minimum that would push
304
+ // content past the row boundary.
305
+ minHeight: fillHeight ? 0 : 70,
290
306
  }}
291
307
  className={`rounded-lg border-2 border-dashed text-xs font-medium transition-all flex items-center justify-center cursor-pointer ${
292
308
  isGapTarget
@@ -294,7 +310,7 @@ export default function SectionV2Canvas({
294
310
  : showAsDropTarget
295
311
  ? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
296
312
  : isSectionHovered
297
- ? "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"
298
314
  : "border-transparent text-transparent opacity-0 pointer-events-none"
299
315
  }`}
300
316
  >
@@ -12,6 +12,7 @@ import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanit
12
12
  import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
13
13
  import { isSectionBlockType } from "../../lib/builder/types";
14
14
  import { BUILDER_BLUE } from "../../lib/builder/constants";
15
+ import { BubbleTooltip, CloseIcon, DragDropIcon } from "./BubbleIcons";
15
16
 
16
17
  // ============================================
17
18
  // SectionV2Column — Individual column in a V2 section grid
@@ -84,17 +85,17 @@ function ResizeHandle({
84
85
  height: isActive ? 16 : isHoveredEdge ? 56 : showChrome ? 56 : 32,
85
86
  borderRadius: isActive ? "50%" : 999,
86
87
  backgroundColor: isActive
87
- ? "rgba(71, 148, 226, 0.9)"
88
+ ? "rgba(53, 128, 249, 0.9)"
88
89
  : isHoveredEdge
89
- ? "rgba(71, 148, 226, 0.7)"
90
+ ? "rgba(53, 128, 249, 0.7)"
90
91
  : showChrome
91
- ? "rgba(71, 148, 226, 0.5)"
92
- : "rgba(71, 148, 226, 0.2)",
92
+ ? "rgba(53, 128, 249, 0.5)"
93
+ : "rgba(53, 128, 249, 0.2)",
93
94
  transition: "width 150ms ease-out, height 150ms ease-out, border-radius 150ms ease-out, background-color 150ms, box-shadow 150ms",
94
95
  boxShadow: isActive
95
- ? "0 0 10px rgba(71, 148, 226, 0.5)"
96
+ ? "0 0 10px rgba(53, 128, 249, 0.5)"
96
97
  : isHoveredEdge
97
- ? "0 0 6px rgba(71, 148, 226, 0.2)"
98
+ ? "0 0 6px rgba(53, 128, 249, 0.2)"
98
99
  : undefined,
99
100
  }}
100
101
  />
@@ -131,6 +132,9 @@ interface SectionV2ColumnProps {
131
132
  onAddBlock: (insertIndex?: number) => void;
132
133
  onResizeRight: (columnKey: string, startX: number, startSpan: number, containerEl: HTMLElement) => void;
133
134
  onResizeLeft: (columnKey: string, startX: number, startGridCol: number, startSpan: number, containerEl: HTMLElement) => void;
135
+ /** When true, the column lives in a Cover row with strict proportional height —
136
+ * empty-state minHeight is relaxed so the column doesn't overflow its row. */
137
+ fillHeight?: boolean;
134
138
  children: ReactNode;
135
139
  }
136
140
 
@@ -152,6 +156,7 @@ export default function SectionV2Column({
152
156
  onAddBlock,
153
157
  onResizeRight,
154
158
  onResizeLeft,
159
+ fillHeight = false,
155
160
  children,
156
161
  }: SectionV2ColumnProps) {
157
162
  const previewMode = useBuilderStore((s) => s.previewMode);
@@ -285,17 +290,17 @@ export default function SectionV2Column({
285
290
  <div
286
291
  className="pointer-events-none absolute inset-0 z-[1] rounded"
287
292
  style={{
288
- transition: "box-shadow 150ms, border 150ms",
293
+ transition: "box-shadow 150ms",
289
294
  ...(isSwapTarget
290
- ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(71, 148, 226, 0.08)" }
295
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(53, 128, 249, 0.08)" }
291
296
  : isBlockOver
292
297
  ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
293
298
  : isSelected
294
- ? { boxShadow: `inset 0 0 0 2px rgba(71, 148, 226, 0.6)` }
299
+ ? { boxShadow: `inset 0 0 0 2px rgba(53, 128, 249, 0.6)` }
295
300
  : isHovered
296
- ? { boxShadow: `inset 0 0 0 1.5px rgba(71, 148, 226, 0.5)` }
301
+ ? { boxShadow: `inset 0 0 0 1.5px rgba(53, 128, 249, 0.5)` }
297
302
  : showFaintOutline
298
- ? { border: `1px dashed rgba(71, 148, 226, 0.2)`, borderRadius: 4 }
303
+ ? { boxShadow: `inset 0 0 0 1px rgba(53, 128, 249, 0.2)`, borderRadius: 4 }
299
304
  : undefined),
300
305
  }}
301
306
  />
@@ -350,64 +355,65 @@ export default function SectionV2Column({
350
355
  </>
351
356
  )}
352
357
 
353
- {/* Delete buttonred circle top right, positioned outside the column box.
358
+ {/* Column pillsingle horizontal pill sitting OUTSIDE the column top-left edge,
359
+ with a 2px gap so its border never touches the column outline. Structure:
360
+ [drag-drop] | Col | [X]. Aligns horizontally with the block pill when the
361
+ first block in the column has no vertical offset.
354
362
  Nested pattern: outer div positions, inner div counter-scales. */}
355
363
  <div
356
- className={`absolute z-[6] transition-opacity ${
364
+ className={`absolute top-0 left-0 z-[6] transition-opacity ${
357
365
  showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
358
366
  }`}
359
- style={{
360
- top: 0,
361
- right: 0,
362
- transform: "translate(40%, -40%)",
363
- }}
367
+ style={{ transform: "translateY(calc(-100% - 2px))" }}
364
368
  onClick={(e) => e.stopPropagation()}
365
369
  >
366
- <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
367
- <button
368
- onClick={handleDelete}
369
- className="w-5 h-5 rounded-full text-white flex items-center justify-center transition-transform hover:scale-[1.15]"
370
- style={{ background: "#ef4848" }}
371
- title="Delete column"
372
- aria-label="Delete column"
373
- >
374
- <svg width="10" height="10" viewBox="0 0 10 10">
375
- <path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
376
- </svg>
377
- </button>
378
- </div>
379
- </div>
380
-
381
- {/* Drag grip — top left corner, uses @dnd-kit useDraggable.
382
- Nested pattern: outer div positions, inner div counter-scales. */}
383
- <div
384
- className={`absolute z-[6] transition-opacity ${
385
- showChrome ? "opacity-100" : "opacity-0 pointer-events-none"
386
- }`}
387
- style={{
388
- top: 0,
389
- left: 0,
390
- transform: "translate(-30%, -30%)",
391
- }}
392
- >
393
- <div style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "center" }}>
394
- <div
395
- 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"
396
- title="Drag to move column"
397
- aria-label="Move column"
398
- onMouseDown={(e) => {
399
- e.stopPropagation();
400
- onStartDrag?.(e);
401
- }}
402
- onClick={(e) => {
403
- e.stopPropagation();
404
- onSelect();
405
- }}
406
- >
407
- <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
408
- <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" />
409
- </svg>
410
- </div>
370
+ <div style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "bottom left" }}>
371
+ <div
372
+ className="flex items-center rounded-[5px]"
373
+ style={{
374
+ background: "#c0d7ff",
375
+ border: "1.5px solid #3580f9",
376
+ }}
377
+ >
378
+ {/* Drag handle starts a column drag via @dnd-kit, selects on click */}
379
+ <button
380
+ type="button"
381
+ 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]"
382
+ aria-label="Move column"
383
+ onMouseDown={(e) => {
384
+ e.stopPropagation();
385
+ onStartDrag?.(e);
386
+ }}
387
+ onClick={(e) => {
388
+ e.stopPropagation();
389
+ onSelect();
390
+ }}
391
+ >
392
+ <DragDropIcon size={14} />
393
+ <BubbleTooltip>Drag to move</BubbleTooltip>
394
+ </button>
395
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
396
+ {/* Label — clicking selects the column */}
397
+ <button
398
+ type="button"
399
+ onClick={(e) => { e.stopPropagation(); onSelect(); }}
400
+ className="text-[11px] px-2 py-0.5 font-medium hover:bg-[#3580f9]/15 transition-colors"
401
+ style={{ color: "#3580f9" }}
402
+ aria-label="Select column"
403
+ >
404
+ Col
405
+ </button>
406
+ <div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
407
+ {/* Delete */}
408
+ <button
409
+ onClick={handleDelete}
410
+ 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]"
411
+ aria-label="Delete column"
412
+ >
413
+ <CloseIcon size={14} />
414
+ <BubbleTooltip>Delete</BubbleTooltip>
415
+ </button>
416
+ </div>
411
417
  </div>
412
418
  </div>
413
419
 
@@ -453,7 +459,7 @@ export default function SectionV2Column({
453
459
  /* Empty column: show + Add Block (flex-1 stretches in fillHeight cover sections) */
454
460
  <div
455
461
  className="relative flex items-center justify-center flex-1"
456
- style={{ minHeight: 80, padding: "16px 12px" }}
462
+ style={{ minHeight: fillHeight ? 0 : 80, padding: "16px 12px" }}
457
463
  >
458
464
  <button
459
465
  onClick={handleAddBlockEmpty}
@@ -469,8 +475,8 @@ export default function SectionV2Column({
469
475
  padding: "5px 16px",
470
476
  pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
471
477
  background: "#d2e3ff",
472
- color: "#4794e2",
473
- border: "1px dashed #4794e2",
478
+ color: "#3580f9",
479
+ border: "1.5px dashed #3580f9",
474
480
  }}
475
481
  >
476
482
  + Add Block
@@ -498,8 +504,8 @@ export default function SectionV2Column({
498
504
  padding: "4px 14px",
499
505
  pointerEvents: showChrome ? "auto" : "none",
500
506
  background: "#d2e3ff",
501
- color: "#4794e2",
502
- border: "1px dashed #4794e2",
507
+ color: "#3580f9",
508
+ border: "1.5px dashed #3580f9",
503
509
  }}
504
510
  >
505
511
  + Add Block
@@ -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="text-[10px] px-1 py-0.5"
193
+ style={{ color: "#3580f9" }}
194
+ title={`Animation: ${block.enter_animation.preset}`}
195
+ >
196
+
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