@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.
Files changed (49) hide show
  1. package/README.md +3 -0
  2. package/components/admin/nav-builder/NavBuilder.tsx +90 -14
  3. package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
  4. package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
  5. package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
  6. package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
  7. package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
  8. package/components/blocks/TextBlockRenderer.tsx +1 -1
  9. package/components/builder/SettingsPanel.tsx +29 -543
  10. package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
  11. package/components/builder/editors/CoverBlockEditor.tsx +14 -6
  12. package/components/builder/editors/ImageBlockEditor.tsx +8 -3
  13. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
  14. package/components/builder/editors/ProjectGridEditor.tsx +7 -46
  15. package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
  16. package/components/builder/editors/StaggerSettings.tsx +2 -1
  17. package/components/builder/editors/TextBlockEditor.tsx +8 -3
  18. package/components/builder/editors/VideoBlockEditor.tsx +10 -4
  19. package/components/builder/editors/section-icons.tsx +492 -0
  20. package/components/builder/editors/shared.tsx +23 -4
  21. package/components/builder/live-preview/GhostCard.tsx +84 -0
  22. package/components/builder/live-preview/LiveProjectGridPreview.tsx +294 -1010
  23. package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
  24. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -0
  25. package/components/builder/live-preview/drag-utils.tsx +89 -0
  26. package/components/builder/live-preview/useDragReorder.ts +370 -0
  27. package/components/builder/settings-panel/AnimationTab.tsx +152 -0
  28. package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
  29. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -0
  30. package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +32 -0
  31. package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
  32. package/components/builder/settings-panel/CustomSectionSettings.tsx +150 -0
  33. package/components/builder/settings-panel/LayoutTab.tsx +11 -47
  34. package/components/builder/settings-panel/PageSettings.tsx +10 -4
  35. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
  36. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
  37. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
  38. package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
  39. package/components/builder/settings-panel/index.ts +6 -0
  40. package/components/builder/settings-panel/useSettingsPanelSelection.ts +184 -0
  41. package/components/ui/Navbar.tsx +151 -30
  42. package/lib/builder/serializer/migrations.ts +107 -0
  43. package/lib/builder/serializer/normalizers.ts +278 -0
  44. package/lib/builder/serializer/serializers.ts +393 -0
  45. package/lib/builder/serializer/shared.ts +102 -0
  46. package/lib/builder/serializer.ts +11 -846
  47. package/lib/sanity/types.ts +22 -0
  48. package/package.json +13 -10
  49. 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: "break-word",
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
+ }