@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
@@ -1,291 +1,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
-
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
+
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(53, 128, 249,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(53, 128, 249,0.30)",
282
+ borderRadius: br,
283
+ pointerEvents: "none",
284
+ zIndex: 5,
285
+ border: `2px solid ${DROP_BLUE}`,
286
+ }}
287
+ />
288
+ )}
289
+ </div>
290
+ );
291
+ }