@morphika/andami 0.5.1 → 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 (117) hide show
  1. package/app/admin/assets/page.tsx +6 -6
  2. package/app/admin/database/page.tsx +302 -302
  3. package/app/admin/error.tsx +53 -53
  4. package/app/admin/layout.tsx +320 -320
  5. package/app/admin/navigation/page.tsx +255 -255
  6. package/app/admin/pages/[slug]/page.tsx +6 -6
  7. package/app/admin/pages/page.tsx +11 -11
  8. package/app/admin/projects/page.tsx +14 -14
  9. package/app/admin/setup/page.tsx +1 -1
  10. package/app/admin/styles/page.tsx +1 -1
  11. package/components/admin/MetadataEditor.tsx +6 -6
  12. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  13. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  14. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  15. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  16. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  17. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  18. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  19. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  20. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  22. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  23. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  24. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  25. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  26. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  27. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  28. package/components/admin/styles/ColorsEditor.tsx +2 -2
  29. package/components/admin/styles/FontsEditor.tsx +6 -6
  30. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  31. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  32. package/components/admin/styles/TypographyEditor.tsx +6 -6
  33. package/components/admin/styles/shared.tsx +68 -68
  34. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  35. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  36. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  37. package/components/builder/BlockCardIcons.tsx +316 -316
  38. package/components/builder/BlockTypePicker.tsx +1 -1
  39. package/components/builder/BubbleIcons.tsx +90 -0
  40. package/components/builder/BuilderCanvas.tsx +2 -0
  41. package/components/builder/CanvasMinimap.tsx +2 -2
  42. package/components/builder/CoverSectionCanvas.tsx +363 -363
  43. package/components/builder/DeviceFrame.tsx +1 -1
  44. package/components/builder/DndWrapper.tsx +3 -3
  45. package/components/builder/InsertionLines.tsx +1 -1
  46. package/components/builder/SectionCardIcons.tsx +421 -320
  47. package/components/builder/SectionEditorBar.tsx +1 -1
  48. package/components/builder/SectionTypePicker.tsx +4 -4
  49. package/components/builder/SectionV2Canvas.tsx +1 -1
  50. package/components/builder/SectionV2Column.tsx +69 -67
  51. package/components/builder/SortableBlock.tsx +93 -73
  52. package/components/builder/SortableRow.tsx +27 -26
  53. package/components/builder/VirtualAssetGrid.tsx +2 -2
  54. package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
  55. package/components/builder/blockStyles.tsx +192 -185
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  57. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  58. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  59. package/components/builder/color-picker/HueSlider.tsx +124 -124
  60. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  61. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  62. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  63. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  64. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  65. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  66. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  67. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  68. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  69. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  70. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  71. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  72. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  73. package/components/builder/editors/StaggerSettings.tsx +109 -109
  74. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  75. package/components/builder/editors/TextStylePicker.tsx +1 -1
  76. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  77. package/components/builder/editors/index.ts +11 -10
  78. package/components/builder/editors/shared.tsx +6 -6
  79. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  80. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  81. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  82. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  83. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  84. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  85. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  86. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  87. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  88. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  89. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  90. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  91. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  92. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  93. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  94. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  95. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  96. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  97. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  98. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  99. package/lib/animation/enter-types.ts +1 -0
  100. package/lib/animation/hover-effect-presets.ts +210 -210
  101. package/lib/animation/hover-effect-types.ts +1 -0
  102. package/lib/builder/block-registrations.ts +468 -417
  103. package/lib/builder/constants.ts +111 -111
  104. package/lib/builder/store-sections.ts +2 -2
  105. package/lib/builder/types-slices.ts +414 -414
  106. package/lib/builder/types.ts +4 -1
  107. package/lib/config/index.ts +27 -27
  108. package/lib/sanity/types.ts +98 -1
  109. package/lib/version.ts +1 -1
  110. package/package.json +1 -1
  111. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  112. package/sanity/schemas/blocks/index.ts +12 -11
  113. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  114. package/sanity/schemas/index.ts +120 -117
  115. package/styles/admin.css +85 -85
  116. package/styles/animations.css +237 -237
  117. 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
+ }