@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
@@ -1,291 +1,293 @@
1
- "use client";
2
-
3
- /**
4
- * ProjectCardWrapper — Renders a single masonry card in 3 visual states:
5
- * grabbed (darkened + cross-arrow icon), dragging/placeholder (dashed border),
6
- * and normal (hover/selection/drop-target overlays).
7
- *
8
- * Session 162: Extracted from LiveProjectGridPreview.tsx (Phase B2).
9
- */
10
-
11
- import { useState, useCallback, useRef } from "react";
12
- import { ProjectGridCard } from "./shared";
13
- import { useBuilderStore } from "../../../lib/builder/store";
14
- import { ADMIN_BLUE, DROP_BLUE, CrossArrowIcon } from "./drag-utils";
15
- import type { ProjectGridItem } from "../../../lib/sanity/types";
16
-
17
- // ─── Props ───────────────────────────────────────────────────────────
18
-
19
- export interface CardProps {
20
- item: ProjectGridItem;
21
- thumbMap: Map<string, string | undefined>;
22
- borderRadius: number;
23
- cardWidth: number;
24
- cardHeight: number;
25
- isGrabbed: boolean;
26
- isDragging: boolean;
27
- isDropTarget: boolean;
28
- isSelected: boolean;
29
- isAnyDragActive: boolean;
30
- onPointerDown: (
31
- key: string,
32
- e: React.PointerEvent,
33
- cardEl: HTMLDivElement,
34
- fromHandle: boolean,
35
- ) => void;
36
- onSelect: (key: string) => void;
37
- }
38
-
39
- // ─── Component ───────────────────────────────────────────────────────
40
-
41
- export default function ProjectCardWrapper({
42
- item,
43
- thumbMap,
44
- borderRadius,
45
- cardWidth,
46
- cardHeight,
47
- isGrabbed,
48
- isDragging,
49
- isDropTarget,
50
- isSelected,
51
- isAnyDragActive,
52
- onPointerDown,
53
- onSelect,
54
- }: CardProps) {
55
- const cardRef = useRef<HTMLDivElement>(null);
56
- const canvasZoom = useBuilderStore((s) => s.canvasZoom);
57
- const [isHovered, setIsHovered] = useState(false);
58
-
59
- // ── Handle pointerdown (drag handle = immediate, card body = hold) ──
60
-
61
- const handleHandleDown = useCallback(
62
- (e: React.PointerEvent) => {
63
- e.preventDefault();
64
- e.stopPropagation();
65
- if (cardRef.current) onPointerDown(item._key, e, cardRef.current, true);
66
- },
67
- [item._key, onPointerDown],
68
- );
69
-
70
- const handleCardDown = useCallback(
71
- (e: React.PointerEvent) => {
72
- if (e.button !== 0) return;
73
- e.stopPropagation();
74
- if (cardRef.current) onPointerDown(item._key, e, cardRef.current, false);
75
- },
76
- [item._key, onPointerDown],
77
- );
78
-
79
- const handleClick = useCallback(
80
- (e: React.MouseEvent) => {
81
- e.stopPropagation();
82
- onSelect(item._key);
83
- },
84
- [item._key, onSelect],
85
- );
86
-
87
- const br = borderRadius > 0 ? borderRadius : undefined;
88
- const brStr = borderRadius > 0 ? String(borderRadius) : undefined;
89
- const invZoom = Math.min(2, 1 / canvasZoom);
90
-
91
- // ── Grabbed: darkened card + centered cross-arrow icon ──────────
92
-
93
- if (isGrabbed) {
94
- return (
95
- <div
96
- ref={cardRef}
97
- style={{
98
- position: "relative",
99
- width: cardWidth,
100
- height: cardHeight,
101
- borderRadius: br,
102
- overflow: "hidden",
103
- outline: `2px solid ${ADMIN_BLUE}`,
104
- outlineOffset: -2,
105
- }}
106
- >
107
- <ProjectGridCard
108
- slug={item.project_slug}
109
- thumbPath={thumbMap.get(item.project_slug)}
110
- customThumb={item.custom_thumbnail}
111
- borderRadius={brStr}
112
- style={{ width: "100%", height: "100%", filter: "brightness(0.65)" }}
113
- />
114
- {/* Centered drag icon overlay */}
115
- <div
116
- style={{
117
- position: "absolute",
118
- inset: 0,
119
- display: "flex",
120
- alignItems: "center",
121
- justifyContent: "center",
122
- pointerEvents: "none",
123
- }}
124
- >
125
- <div
126
- style={{
127
- width: 40,
128
- height: 40,
129
- borderRadius: "50%",
130
- backgroundColor: "rgba(255,255,255,0.92)",
131
- display: "flex",
132
- alignItems: "center",
133
- justifyContent: "center",
134
- boxShadow: "0 2px 12px rgba(0,0,0,0.2)",
135
- transform: `scale(${invZoom})`,
136
- }}
137
- >
138
- <CrossArrowIcon size={20} color="#333" />
139
- </div>
140
- </div>
141
- </div>
142
- );
143
- }
144
-
145
- // ── Dragging / Cancelling: dashed blue placeholder ─────────────
146
-
147
- if (isDragging) {
148
- return (
149
- <div
150
- ref={cardRef}
151
- style={{
152
- width: cardWidth,
153
- height: cardHeight,
154
- borderRadius: br,
155
- border: `${Math.max(2, 2 / canvasZoom)}px dashed ${ADMIN_BLUE}`,
156
- backgroundColor: "transparent",
157
- boxSizing: "border-box",
158
- }}
159
- />
160
- );
161
- }
162
-
163
- // ── Normal card ────────────────────────────────────────────────
164
-
165
- // Green drop-target highlight — no extra style on wrapper, overlay rendered below
166
- const dropStyle: React.CSSProperties = {};
167
-
168
- // Blue hover border (suppressed when drag is active or card is selected/drop target)
169
- const showHover = isHovered && !isSelected && !isDropTarget && !isAnyDragActive;
170
- const hoverStyle: React.CSSProperties = showHover
171
- ? {
172
- outline: `${2 / canvasZoom}px solid ${ADMIN_BLUE}`,
173
- outlineOffset: -2 / canvasZoom,
174
- }
175
- : {};
176
-
177
- // Blue selection border + subtle tint
178
- const selectStyle: React.CSSProperties =
179
- isSelected && !isDropTarget
180
- ? {
181
- outline: `2px solid ${ADMIN_BLUE}`,
182
- outlineOffset: -2,
183
- boxShadow: "inset 0 0 0 9999px rgba(7,107,255,0.04)",
184
- }
185
- : {};
186
-
187
- return (
188
- <div
189
- ref={cardRef}
190
- style={{
191
- position: "relative",
192
- width: cardWidth,
193
- height: cardHeight,
194
- cursor: "pointer",
195
- borderRadius: br,
196
- overflow: "hidden",
197
- ...dropStyle,
198
- ...hoverStyle,
199
- ...selectStyle,
200
- transition: "outline 150ms ease, box-shadow 150ms ease",
201
- }}
202
- onPointerDown={handleCardDown}
203
- onClick={handleClick}
204
- onMouseEnter={() => setIsHovered(true)}
205
- onMouseLeave={() => setIsHovered(false)}
206
- >
207
- {/* Drag handle — centered, visible on hover/selection (hidden during drag) */}
208
- <div
209
- className={`absolute z-10 transition-opacity ${
210
- (isHovered || isSelected) && !isAnyDragActive
211
- ? "opacity-100"
212
- : "opacity-0 pointer-events-none"
213
- }`}
214
- style={{
215
- inset: 0,
216
- display: "flex",
217
- alignItems: "center",
218
- justifyContent: "center",
219
- }}
220
- >
221
- <div
222
- onPointerDown={handleHandleDown}
223
- onClick={(e) => e.stopPropagation()}
224
- style={{
225
- width: 48,
226
- height: 48,
227
- borderRadius: "50%",
228
- backgroundColor: "rgba(255,255,255,0.92)",
229
- display: "flex",
230
- alignItems: "center",
231
- justifyContent: "center",
232
- boxShadow: "0 2px 12px rgba(0,0,0,0.25)",
233
- cursor: "grab",
234
- transform: `scale(${invZoom})`,
235
- }}
236
- title="Drag to reorder"
237
- aria-label="Drag to reorder project"
238
- >
239
- <CrossArrowIcon size={22} color="#333" />
240
- </div>
241
- </div>
242
-
243
- {/* Per-card aspect ratio override badge — bottom-right */}
244
- {item.aspect_ratio_override && (
245
- <div
246
- className="absolute z-10"
247
- style={{
248
- bottom: 8,
249
- right: 8,
250
- transform: `scale(${invZoom})`,
251
- transformOrigin: "bottom right",
252
- }}
253
- >
254
- <span
255
- className="px-1.5 py-0.5 rounded text-[9px] font-medium"
256
- style={{
257
- backgroundColor: "rgba(0,0,0,0.6)",
258
- color: "rgba(255,255,255,0.85)",
259
- backdropFilter: "blur(4px)",
260
- }}
261
- >
262
- {item.aspect_ratio_override.replace("/", ":")}
263
- </span>
264
- </div>
265
- )}
266
-
267
- <ProjectGridCard
268
- slug={item.project_slug}
269
- thumbPath={thumbMap.get(item.project_slug)}
270
- customThumb={item.custom_thumbnail}
271
- borderRadius={brStr}
272
- style={{ width: "100%", height: "100%" }}
273
- />
274
-
275
- {/* Green drop-target overlay (covers the image) */}
276
- {isDropTarget && (
277
- <div
278
- style={{
279
- position: "absolute",
280
- inset: 0,
281
- backgroundColor: "rgba(7,107,255,0.30)",
282
- borderRadius: br,
283
- pointerEvents: "none",
284
- zIndex: 5,
285
- border: `2px solid ${DROP_BLUE}`,
286
- }}
287
- />
288
- )}
289
- </div>
290
- );
291
- }
1
+ "use client";
2
+
3
+ /**
4
+ * ProjectCardWrapper — Renders a single masonry card in 3 visual states:
5
+ * grabbed (darkened + cross-arrow icon), dragging/placeholder (dashed border),
6
+ * and normal (hover/selection/drop-target overlays).
7
+ *
8
+ * Session 162: Extracted from LiveProjectGridPreview.tsx (Phase B2).
9
+ */
10
+
11
+ import { useState, useCallback, useRef } from "react";
12
+ import { ProjectGridCard } from "./shared";
13
+ import { useBuilderStore } from "../../../lib/builder/store";
14
+ import { ADMIN_BLUE, DROP_BLUE, CrossArrowIcon } from "./drag-utils";
15
+ import type { ProjectGridItem } from "../../../lib/sanity/types";
16
+ import { BubbleTooltip } from "../BubbleIcons";
17
+
18
+ // ─── Props ───────────────────────────────────────────────────────────
19
+
20
+ export interface CardProps {
21
+ item: ProjectGridItem;
22
+ thumbMap: Map<string, string | undefined>;
23
+ borderRadius: number;
24
+ cardWidth: number;
25
+ cardHeight: number;
26
+ isGrabbed: boolean;
27
+ isDragging: boolean;
28
+ isDropTarget: boolean;
29
+ isSelected: boolean;
30
+ isAnyDragActive: boolean;
31
+ onPointerDown: (
32
+ key: string,
33
+ e: React.PointerEvent,
34
+ cardEl: HTMLDivElement,
35
+ fromHandle: boolean,
36
+ ) => void;
37
+ onSelect: (key: string) => void;
38
+ }
39
+
40
+ // ─── Component ───────────────────────────────────────────────────────
41
+
42
+ export default function ProjectCardWrapper({
43
+ item,
44
+ thumbMap,
45
+ borderRadius,
46
+ cardWidth,
47
+ cardHeight,
48
+ isGrabbed,
49
+ isDragging,
50
+ isDropTarget,
51
+ isSelected,
52
+ isAnyDragActive,
53
+ onPointerDown,
54
+ onSelect,
55
+ }: CardProps) {
56
+ const cardRef = useRef<HTMLDivElement>(null);
57
+ const canvasZoom = useBuilderStore((s) => s.canvasZoom);
58
+ const [isHovered, setIsHovered] = useState(false);
59
+
60
+ // ── Handle pointerdown (drag handle = immediate, card body = hold) ──
61
+
62
+ const handleHandleDown = useCallback(
63
+ (e: React.PointerEvent) => {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ if (cardRef.current) onPointerDown(item._key, e, cardRef.current, true);
67
+ },
68
+ [item._key, onPointerDown],
69
+ );
70
+
71
+ const handleCardDown = useCallback(
72
+ (e: React.PointerEvent) => {
73
+ if (e.button !== 0) return;
74
+ e.stopPropagation();
75
+ if (cardRef.current) onPointerDown(item._key, e, cardRef.current, false);
76
+ },
77
+ [item._key, onPointerDown],
78
+ );
79
+
80
+ const handleClick = useCallback(
81
+ (e: React.MouseEvent) => {
82
+ e.stopPropagation();
83
+ onSelect(item._key);
84
+ },
85
+ [item._key, onSelect],
86
+ );
87
+
88
+ const br = borderRadius > 0 ? borderRadius : undefined;
89
+ const brStr = borderRadius > 0 ? String(borderRadius) : undefined;
90
+ const invZoom = Math.min(2, 1 / canvasZoom);
91
+
92
+ // ── Grabbed: darkened card + centered cross-arrow icon ──────────
93
+
94
+ if (isGrabbed) {
95
+ return (
96
+ <div
97
+ ref={cardRef}
98
+ style={{
99
+ position: "relative",
100
+ width: cardWidth,
101
+ height: cardHeight,
102
+ borderRadius: br,
103
+ overflow: "hidden",
104
+ outline: `2px solid ${ADMIN_BLUE}`,
105
+ outlineOffset: -2,
106
+ }}
107
+ >
108
+ <ProjectGridCard
109
+ slug={item.project_slug}
110
+ thumbPath={thumbMap.get(item.project_slug)}
111
+ customThumb={item.custom_thumbnail}
112
+ borderRadius={brStr}
113
+ style={{ width: "100%", height: "100%", filter: "brightness(0.65)" }}
114
+ />
115
+ {/* Centered drag icon overlay */}
116
+ <div
117
+ style={{
118
+ position: "absolute",
119
+ inset: 0,
120
+ display: "flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ pointerEvents: "none",
124
+ }}
125
+ >
126
+ <div
127
+ style={{
128
+ width: 40,
129
+ height: 40,
130
+ borderRadius: "50%",
131
+ backgroundColor: "rgba(255,255,255,0.92)",
132
+ display: "flex",
133
+ alignItems: "center",
134
+ justifyContent: "center",
135
+ boxShadow: "0 2px 12px rgba(0,0,0,0.2)",
136
+ transform: `scale(${invZoom})`,
137
+ }}
138
+ >
139
+ <CrossArrowIcon size={20} color="#333" />
140
+ </div>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ // ── Dragging / Cancelling: dashed blue placeholder ─────────────
147
+
148
+ if (isDragging) {
149
+ return (
150
+ <div
151
+ ref={cardRef}
152
+ style={{
153
+ width: cardWidth,
154
+ height: cardHeight,
155
+ borderRadius: br,
156
+ border: `${Math.max(2, 2 / canvasZoom)}px dashed ${ADMIN_BLUE}`,
157
+ backgroundColor: "transparent",
158
+ boxSizing: "border-box",
159
+ }}
160
+ />
161
+ );
162
+ }
163
+
164
+ // ── Normal card ────────────────────────────────────────────────
165
+
166
+ // Green drop-target highlight — no extra style on wrapper, overlay rendered below
167
+ const dropStyle: React.CSSProperties = {};
168
+
169
+ // Blue hover border (suppressed when drag is active or card is selected/drop target)
170
+ const showHover = isHovered && !isSelected && !isDropTarget && !isAnyDragActive;
171
+ const hoverStyle: React.CSSProperties = showHover
172
+ ? {
173
+ outline: `${2 / canvasZoom}px solid ${ADMIN_BLUE}`,
174
+ outlineOffset: -2 / canvasZoom,
175
+ }
176
+ : {};
177
+
178
+ // Blue selection border + subtle tint
179
+ const selectStyle: React.CSSProperties =
180
+ isSelected && !isDropTarget
181
+ ? {
182
+ outline: `2px solid ${ADMIN_BLUE}`,
183
+ outlineOffset: -2,
184
+ boxShadow: "inset 0 0 0 9999px rgba(53, 128, 249,0.04)",
185
+ }
186
+ : {};
187
+
188
+ return (
189
+ <div
190
+ ref={cardRef}
191
+ style={{
192
+ position: "relative",
193
+ width: cardWidth,
194
+ height: cardHeight,
195
+ cursor: "pointer",
196
+ borderRadius: br,
197
+ overflow: "hidden",
198
+ ...dropStyle,
199
+ ...hoverStyle,
200
+ ...selectStyle,
201
+ transition: "outline 150ms ease, box-shadow 150ms ease",
202
+ }}
203
+ onPointerDown={handleCardDown}
204
+ onClick={handleClick}
205
+ onMouseEnter={() => setIsHovered(true)}
206
+ onMouseLeave={() => setIsHovered(false)}
207
+ >
208
+ {/* Drag handle — centered, visible on hover/selection (hidden during drag) */}
209
+ <div
210
+ className={`absolute z-10 transition-opacity ${
211
+ (isHovered || isSelected) && !isAnyDragActive
212
+ ? "opacity-100"
213
+ : "opacity-0 pointer-events-none"
214
+ }`}
215
+ style={{
216
+ inset: 0,
217
+ display: "flex",
218
+ alignItems: "center",
219
+ justifyContent: "center",
220
+ }}
221
+ >
222
+ <div
223
+ onPointerDown={handleHandleDown}
224
+ onClick={(e) => e.stopPropagation()}
225
+ className="group/bb relative"
226
+ style={{
227
+ width: 48,
228
+ height: 48,
229
+ borderRadius: "50%",
230
+ backgroundColor: "rgba(255,255,255,0.92)",
231
+ display: "flex",
232
+ alignItems: "center",
233
+ justifyContent: "center",
234
+ boxShadow: "0 2px 12px rgba(0,0,0,0.25)",
235
+ cursor: "grab",
236
+ transform: `scale(${invZoom})`,
237
+ }}
238
+ aria-label="Drag to reorder project"
239
+ >
240
+ <CrossArrowIcon size={22} color="#333" />
241
+ <BubbleTooltip>Drag to reorder</BubbleTooltip>
242
+ </div>
243
+ </div>
244
+
245
+ {/* Per-card aspect ratio override badge — bottom-right */}
246
+ {item.aspect_ratio_override && (
247
+ <div
248
+ className="absolute z-10"
249
+ style={{
250
+ bottom: 8,
251
+ right: 8,
252
+ transform: `scale(${invZoom})`,
253
+ transformOrigin: "bottom right",
254
+ }}
255
+ >
256
+ <span
257
+ className="px-1.5 py-0.5 rounded text-[9px] font-medium"
258
+ style={{
259
+ backgroundColor: "rgba(0,0,0,0.6)",
260
+ color: "rgba(255,255,255,0.85)",
261
+ backdropFilter: "blur(4px)",
262
+ }}
263
+ >
264
+ {item.aspect_ratio_override.replace("/", ":")}
265
+ </span>
266
+ </div>
267
+ )}
268
+
269
+ <ProjectGridCard
270
+ slug={item.project_slug}
271
+ thumbPath={thumbMap.get(item.project_slug)}
272
+ customThumb={item.custom_thumbnail}
273
+ borderRadius={brStr}
274
+ style={{ width: "100%", height: "100%" }}
275
+ />
276
+
277
+ {/* Green drop-target overlay (covers the image) */}
278
+ {isDropTarget && (
279
+ <div
280
+ style={{
281
+ position: "absolute",
282
+ inset: 0,
283
+ backgroundColor: "rgba(53, 128, 249,0.30)",
284
+ borderRadius: br,
285
+ pointerEvents: "none",
286
+ zIndex: 5,
287
+ border: `2px solid ${DROP_BLUE}`,
288
+ }}
289
+ />
290
+ )}
291
+ </div>
292
+ );
293
+ }
@@ -12,6 +12,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
12
12
  import { BubbleMenu, type Editor } from "@tiptap/react";
13
13
  import { UnifiedColorPicker } from "../color-picker";
14
14
  import { usePaletteSwatches } from "../ColorSwatchPicker";
15
+ import { BubbleTooltipAbove } from "../BubbleIcons";
15
16
  import type { ColorField } from "../../../lib/sanity/types";
16
17
 
17
18
  // ── Icon components (inline SVGs to avoid external deps) ────────────
@@ -83,9 +84,9 @@ function ToolbarButton({
83
84
  onMouseDown={(e) => e.preventDefault()}
84
85
  onClick={onClick}
85
86
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") onClick(); }}
86
- title={title}
87
+ aria-label={title}
87
88
  className={`
88
- flex items-center justify-center w-7 h-7 rounded cursor-pointer select-none
89
+ group/bb relative flex items-center justify-center w-7 h-7 rounded cursor-pointer select-none
89
90
  transition-colors duration-100
90
91
  ${isActive
91
92
  ? "bg-white/20 text-white"
@@ -94,6 +95,7 @@ function ToolbarButton({
94
95
  `}
95
96
  >
96
97
  {children}
98
+ <BubbleTooltipAbove>{title}</BubbleTooltipAbove>
97
99
  </div>
98
100
  );
99
101
  }
@@ -190,11 +192,12 @@ function LinkPopover({
190
192
  tabIndex={0}
191
193
  onClick={handleRemove}
192
194
  onKeyDown={(e) => { if (e.key === "Enter") handleRemove(); }}
193
- title="Remove link"
194
- className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-400/10
195
+ aria-label="Remove link"
196
+ className="group/bb relative p-1.5 text-red-400 hover:text-red-300 hover:bg-red-400/10
195
197
  rounded transition-colors cursor-pointer select-none"
196
198
  >
197
199
  <UnlinkIcon />
200
+ <BubbleTooltipAbove>Remove link</BubbleTooltipAbove>
198
201
  </div>
199
202
  )}
200
203
  </div>
@@ -401,12 +404,13 @@ export default function RichTextBubbleMenu({ editor }: { editor: Editor }) {
401
404
  onMouseDown={(e) => e.preventDefault()}
402
405
  onClick={handleRemoveColor}
403
406
  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleRemoveColor(); }}
404
- title="Remove color"
405
- className="flex items-center justify-center w-5 h-5 rounded cursor-pointer select-none
407
+ aria-label="Remove color"
408
+ className="group/bb relative flex items-center justify-center w-5 h-5 rounded cursor-pointer select-none
406
409
  text-neutral-400 hover:text-red-400 hover:bg-red-400/10
407
410
  transition-colors duration-100 text-[10px]"
408
411
  >
409
412
 
413
+ <BubbleTooltipAbove>Remove color</BubbleTooltipAbove>
410
414
  </div>
411
415
  )}
412
416