@morphika/andami 0.1.8 → 0.1.10
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.
- package/README.md +3 -0
- package/components/admin/nav-builder/NavBuilder.tsx +90 -14
- package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
- package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
- package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
- package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/SettingsPanel.tsx +29 -543
- package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
- package/components/builder/editors/CoverBlockEditor.tsx +14 -6
- package/components/builder/editors/ImageBlockEditor.tsx +8 -3
- package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
- package/components/builder/editors/ProjectGridEditor.tsx +7 -46
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
- package/components/builder/editors/StaggerSettings.tsx +2 -1
- package/components/builder/editors/TextBlockEditor.tsx +8 -3
- package/components/builder/editors/VideoBlockEditor.tsx +10 -4
- package/components/builder/editors/section-icons.tsx +492 -0
- package/components/builder/editors/shared.tsx +23 -4
- package/components/builder/live-preview/GhostCard.tsx +84 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +294 -1010
- package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -0
- package/components/builder/live-preview/drag-utils.tsx +89 -0
- package/components/builder/live-preview/useDragReorder.ts +370 -0
- package/components/builder/settings-panel/AnimationTab.tsx +152 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -0
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +32 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- package/components/builder/settings-panel/CustomSectionSettings.tsx +150 -0
- package/components/builder/settings-panel/LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/PageSettings.tsx +10 -4
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
- package/components/builder/settings-panel/index.ts +6 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +184 -0
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/serializer/migrations.ts +107 -0
- package/lib/builder/serializer/normalizers.ts +278 -0
- package/lib/builder/serializer/serializers.ts +393 -0
- package/lib/builder/serializer/shared.ts +102 -0
- package/lib/builder/serializer.ts +11 -846
- package/lib/sanity/types.ts +22 -0
- package/package.json +13 -10
- package/styles/base.css +7 -3
|
@@ -149,7 +149,7 @@ export default function LiveTextEditor({ block, editable = false }: { block: Tex
|
|
|
149
149
|
fontFamily: "inherit",
|
|
150
150
|
outline: "none",
|
|
151
151
|
whiteSpace: "pre-wrap",
|
|
152
|
-
wordBreak: "
|
|
152
|
+
wordBreak: "normal",
|
|
153
153
|
minHeight: "1em",
|
|
154
154
|
// Multi-column layout: gap inherits global grid gutter
|
|
155
155
|
...(cols ? {
|
|
@@ -0,0 +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_GREEN, 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(34,197,94,0.30)",
|
|
282
|
+
borderRadius: br,
|
|
283
|
+
pointerEvents: "none",
|
|
284
|
+
zIndex: 5,
|
|
285
|
+
border: `2px solid ${DROP_GREEN}`,
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag & drop utilities for LiveProjectGridPreview.
|
|
3
|
+
*
|
|
4
|
+
* Shared constants, types, and helper functions used by the main component,
|
|
5
|
+
* ProjectCardWrapper, GhostCard, and useDragReorder hook.
|
|
6
|
+
*
|
|
7
|
+
* Session 162: Extracted from LiveProjectGridPreview.tsx (Phase B1).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BUILDER_BLUE, BUILDER_GREEN } from "../../../lib/builder/constants";
|
|
11
|
+
import type { MasonryOutput } from "../../../lib/builder/masonry";
|
|
12
|
+
|
|
13
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export const HOLD_DELAY = 150; // ms before card-body drag activates
|
|
16
|
+
export const MOVE_THRESHOLD_SQ = 9; // 3px² — grabbed → dragging
|
|
17
|
+
export const CANCEL_DURATION = 200; // ms — cancel fly-back animation
|
|
18
|
+
export const ADMIN_BLUE = BUILDER_BLUE;
|
|
19
|
+
export const DROP_GREEN = BUILDER_GREEN;
|
|
20
|
+
|
|
21
|
+
// ─── Types ───────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface DragState {
|
|
24
|
+
phase: "grabbed" | "dragging" | "cancelling";
|
|
25
|
+
draggedKey: string;
|
|
26
|
+
hoverTargetKey: string | null;
|
|
27
|
+
mouseX: number;
|
|
28
|
+
mouseY: number;
|
|
29
|
+
startMouseX: number;
|
|
30
|
+
startMouseY: number;
|
|
31
|
+
offsetX: number; // grab offset inside the card (screen px)
|
|
32
|
+
offsetY: number;
|
|
33
|
+
cardWidth: number; // screen-space dimensions (includes zoom)
|
|
34
|
+
cardHeight: number;
|
|
35
|
+
origScreenX: number; // original card position on screen (for cancel)
|
|
36
|
+
origScreenY: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Utilities ───────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** Cross-arrow icon SVG (reused in handle, grabbed overlay, ghost). */
|
|
42
|
+
export function CrossArrowIcon({
|
|
43
|
+
size = 14,
|
|
44
|
+
color = "currentColor",
|
|
45
|
+
}: {
|
|
46
|
+
size?: number;
|
|
47
|
+
color?: string;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill={color}>
|
|
51
|
+
<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" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hit-test pointer (screen coords) against masonry items (container coords).
|
|
58
|
+
* Returns the key of the card under the cursor, or null if in a gap.
|
|
59
|
+
* Only ONE card can match (masonry cards never overlap).
|
|
60
|
+
*/
|
|
61
|
+
export function hitTestCards(
|
|
62
|
+
clientX: number,
|
|
63
|
+
clientY: number,
|
|
64
|
+
container: HTMLElement,
|
|
65
|
+
containerWidth: number,
|
|
66
|
+
masonry: MasonryOutput,
|
|
67
|
+
excludeKey: string,
|
|
68
|
+
): string | null {
|
|
69
|
+
const rect = container.getBoundingClientRect();
|
|
70
|
+
if (rect.width === 0 || rect.height === 0 || masonry.totalHeight === 0)
|
|
71
|
+
return null;
|
|
72
|
+
|
|
73
|
+
// Convert screen → masonry coordinate space
|
|
74
|
+
const relX = (clientX - rect.left) * (containerWidth / rect.width);
|
|
75
|
+
const relY = (clientY - rect.top) * (masonry.totalHeight / rect.height);
|
|
76
|
+
|
|
77
|
+
for (const item of masonry.items) {
|
|
78
|
+
if (item.key === excludeKey) continue;
|
|
79
|
+
if (
|
|
80
|
+
relX >= item.x &&
|
|
81
|
+
relX <= item.x + item.width &&
|
|
82
|
+
relY >= item.y &&
|
|
83
|
+
relY <= item.y + item.height
|
|
84
|
+
) {
|
|
85
|
+
return item.key;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|