@morphika/andami 0.5.2 → 0.5.4
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 +27 -2
- package/app/admin/layout.tsx +26 -14
- package/app/admin/pages/[slug]/page.tsx +39 -22
- package/app/admin/pages/page.tsx +13 -8
- package/app/admin/projects/page.tsx +17 -8
- package/app/api/admin/assets/register/route.ts +51 -14
- package/app/api/admin/assets/registry/route.ts +4 -1
- package/app/api/admin/assets/relink/confirm/route.ts +4 -1
- package/app/api/admin/assets/relink/route.ts +4 -1
- package/app/api/admin/assets/scan/route.ts +4 -1
- package/app/api/admin/backups/restore-data/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +4 -1
- package/app/api/admin/r2/delete/route.ts +4 -1
- package/app/api/admin/r2/rename/route.ts +4 -1
- package/app/api/admin/r2/upload-url/route.ts +4 -1
- package/app/api/admin/revalidate/route.ts +4 -1
- package/app/api/admin/storage/switch/route.ts +4 -1
- package/app/api/custom-sections/[id]/route.ts +5 -6
- package/components/admin/PublishToggle.tsx +2 -2
- package/components/admin/nav-builder/NavGridItem.tsx +4 -2
- package/components/admin/nav-builder/NavSettingsFields.tsx +10 -6
- package/components/admin/styles/ColorsEditor.tsx +7 -6
- package/components/admin/styles/FontsEditor.tsx +3 -1
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BubbleIcons.tsx +14 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/SectionEditorBar.tsx +4 -2
- package/components/builder/SectionTypePicker.tsx +4 -2
- package/components/builder/SectionV2Column.tsx +13 -1
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +2 -2
- package/components/builder/SortableRow.tsx +6 -9
- package/components/builder/VirtualAssetGrid.tsx +8 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +8 -4
- package/components/builder/color-picker/EyedropperButton.tsx +7 -6
- package/components/builder/color-picker/SwatchBar.tsx +11 -6
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -2
- package/components/builder/editors/MarqueeBlockEditor.tsx +3 -2
- package/components/builder/editors/ProjectGridEditor.tsx +12 -7
- package/components/builder/editors/SpacerBlockEditor.tsx +25 -23
- package/components/builder/editors/TextBlockEditor.tsx +19 -14
- package/components/builder/editors/shared.tsx +4 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +3 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -1
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/BlockLayoutTab.tsx +4 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- package/components/builder/settings-panel/CoverSectionSettings.tsx +4 -2
- package/components/builder/settings-panel/SectionV2Settings.tsx +13 -8
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/ui/NavContentLightbox.tsx +41 -4
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-blocks.ts +15 -5
- package/lib/builder/store-cover.ts +16 -6
- package/lib/builder/store-sections.ts +151 -51
- package/lib/builder/types-slices.ts +14 -0
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +14 -0
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useCallback } from "react";
|
|
3
|
+
import { useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { assetUrl } from "../../lib/assets";
|
|
5
5
|
|
|
6
|
+
const FOCUSABLE_SELECTOR =
|
|
7
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), iframe, [tabindex]:not([tabindex="-1"])';
|
|
8
|
+
|
|
6
9
|
// ============================================
|
|
7
10
|
// Helpers
|
|
8
11
|
// ============================================
|
|
@@ -42,22 +45,51 @@ export default function NavContentLightbox({
|
|
|
42
45
|
contentUrl,
|
|
43
46
|
onClose,
|
|
44
47
|
}: NavContentLightboxProps) {
|
|
45
|
-
|
|
48
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
49
|
+
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
50
|
+
|
|
51
|
+
// Focus trap: cycle Tab between focusable children, close on Escape
|
|
46
52
|
const handleKey = useCallback(
|
|
47
53
|
(e: KeyboardEvent) => {
|
|
48
|
-
if (e.key === "Escape")
|
|
54
|
+
if (e.key === "Escape") {
|
|
55
|
+
onClose();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (e.key !== "Tab" || !containerRef.current) return;
|
|
59
|
+
|
|
60
|
+
const focusables = containerRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
61
|
+
if (focusables.length === 0) return;
|
|
62
|
+
|
|
63
|
+
const first = focusables[0];
|
|
64
|
+
const last = focusables[focusables.length - 1];
|
|
65
|
+
const active = document.activeElement as HTMLElement | null;
|
|
66
|
+
|
|
67
|
+
if (e.shiftKey && active === first) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
last.focus();
|
|
70
|
+
} else if (!e.shiftKey && active === last) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
first.focus();
|
|
73
|
+
}
|
|
49
74
|
},
|
|
50
75
|
[onClose]
|
|
51
76
|
);
|
|
52
77
|
|
|
53
78
|
useEffect(() => {
|
|
79
|
+
// Remember where focus came from so we can restore it on close
|
|
80
|
+
const previouslyFocused = document.activeElement as HTMLElement | null;
|
|
81
|
+
|
|
54
82
|
window.addEventListener("keydown", handleKey);
|
|
55
|
-
// Lock body scroll
|
|
56
83
|
const prev = document.body.style.overflow;
|
|
57
84
|
document.body.style.overflow = "hidden";
|
|
85
|
+
|
|
86
|
+
// Move focus into the lightbox (close button is always present)
|
|
87
|
+
closeButtonRef.current?.focus();
|
|
88
|
+
|
|
58
89
|
return () => {
|
|
59
90
|
window.removeEventListener("keydown", handleKey);
|
|
60
91
|
document.body.style.overflow = prev;
|
|
92
|
+
previouslyFocused?.focus?.();
|
|
61
93
|
};
|
|
62
94
|
}, [handleKey]);
|
|
63
95
|
|
|
@@ -78,11 +110,16 @@ export default function NavContentLightbox({
|
|
|
78
110
|
|
|
79
111
|
return (
|
|
80
112
|
<div
|
|
113
|
+
ref={containerRef}
|
|
114
|
+
role="dialog"
|
|
115
|
+
aria-modal="true"
|
|
116
|
+
aria-label="Media lightbox"
|
|
81
117
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm"
|
|
82
118
|
onClick={onClose}
|
|
83
119
|
>
|
|
84
120
|
{/* Close button */}
|
|
85
121
|
<button
|
|
122
|
+
ref={closeButtonRef}
|
|
86
123
|
onClick={onClose}
|
|
87
124
|
className="absolute top-5 right-5 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors z-10"
|
|
88
125
|
aria-label="Close lightbox"
|
|
@@ -31,6 +31,19 @@ export function normalizePageSectionV2(section: Partial<PageSectionV2> & { _key:
|
|
|
31
31
|
// Migrate column-level enter_animation (new field, no old equivalent for columns)
|
|
32
32
|
const colEnterAnimation = colRecord.enter_animation as EnterAnimationConfig | undefined;
|
|
33
33
|
|
|
34
|
+
// Column-level background + border — carry through from Sanity (desktop-only).
|
|
35
|
+
const colLayoutFields: Partial<SectionColumn> = {};
|
|
36
|
+
for (const field of [
|
|
37
|
+
"background_color", "background_opacity", "background_image",
|
|
38
|
+
"background_size", "background_position", "background_repeat",
|
|
39
|
+
"border_color", "border_width", "border_style", "border_sides", "border_radius",
|
|
40
|
+
] as const) {
|
|
41
|
+
const val = colRecord[field];
|
|
42
|
+
if (val !== undefined && val !== null) {
|
|
43
|
+
(colLayoutFields as Record<string, unknown>)[field] = val;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
return {
|
|
35
48
|
_key: col._key || generateKey(),
|
|
36
49
|
grid_column: col.grid_column || 1,
|
|
@@ -55,6 +68,7 @@ export function normalizePageSectionV2(section: Partial<PageSectionV2> & { _key:
|
|
|
55
68
|
return b as ContentBlock;
|
|
56
69
|
}),
|
|
57
70
|
...(colEnterAnimation ? { enter_animation: colEnterAnimation } : {}),
|
|
71
|
+
...colLayoutFields,
|
|
58
72
|
};
|
|
59
73
|
});
|
|
60
74
|
|
|
@@ -134,6 +134,21 @@ function serializePageSectionV2(section: PageSectionV2): Record<string, unknown>
|
|
|
134
134
|
if (col.enter_animation && col.enter_animation.preset && col.enter_animation.preset !== "none") {
|
|
135
135
|
colData.enter_animation = col.enter_animation;
|
|
136
136
|
}
|
|
137
|
+
// Column-level background + border (desktop-only).
|
|
138
|
+
const colLayout = stripUndefined({
|
|
139
|
+
background_color: col.background_color,
|
|
140
|
+
background_opacity: col.background_opacity,
|
|
141
|
+
background_image: col.background_image,
|
|
142
|
+
background_size: col.background_size,
|
|
143
|
+
background_position: col.background_position,
|
|
144
|
+
background_repeat: col.background_repeat,
|
|
145
|
+
border_color: col.border_color,
|
|
146
|
+
border_width: col.border_width,
|
|
147
|
+
border_style: col.border_style,
|
|
148
|
+
border_sides: col.border_sides,
|
|
149
|
+
border_radius: col.border_radius,
|
|
150
|
+
});
|
|
151
|
+
if (colLayout) Object.assign(colData, colLayout);
|
|
137
152
|
return colData;
|
|
138
153
|
}),
|
|
139
154
|
settings: s ? stripUndefined({
|
|
@@ -223,6 +238,18 @@ function serializeCoverSection(section: CoverSection): Record<string, unknown> {
|
|
|
223
238
|
span: col.span,
|
|
224
239
|
blocks: (col.blocks || []).map((b) => serializeBlock(b)),
|
|
225
240
|
enter_animation: col.enter_animation,
|
|
241
|
+
// Column-level background + border (desktop-only).
|
|
242
|
+
background_color: col.background_color,
|
|
243
|
+
background_opacity: col.background_opacity,
|
|
244
|
+
background_image: col.background_image,
|
|
245
|
+
background_size: col.background_size,
|
|
246
|
+
background_position: col.background_position,
|
|
247
|
+
background_repeat: col.background_repeat,
|
|
248
|
+
border_color: col.border_color,
|
|
249
|
+
border_width: col.border_width,
|
|
250
|
+
border_style: col.border_style,
|
|
251
|
+
border_sides: col.border_sides,
|
|
252
|
+
border_radius: col.border_radius,
|
|
226
253
|
} as Record<string, unknown>));
|
|
227
254
|
|
|
228
255
|
return stripUndefined({
|
|
@@ -60,7 +60,6 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
|
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
deleteBlock: (blockKey: string): void => {
|
|
63
|
-
get()._pushSnapshot();
|
|
64
63
|
const filterBlocks = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
|
|
65
64
|
cols.map((c) => ({ ...c, blocks: c.blocks.filter((b) => b._key !== blockKey) }));
|
|
66
65
|
|
|
@@ -88,12 +87,13 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
|
|
|
88
87
|
rows: finalRows,
|
|
89
88
|
selectedBlockKey: state.selectedBlockKey === blockKey ? null : state.selectedBlockKey,
|
|
90
89
|
isDirty: true,
|
|
90
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
91
|
+
_future: [],
|
|
91
92
|
};
|
|
92
93
|
});
|
|
93
94
|
},
|
|
94
95
|
|
|
95
96
|
duplicateBlock: (blockKey: string): void => {
|
|
96
|
-
get()._pushSnapshot();
|
|
97
97
|
set((state) => {
|
|
98
98
|
const newKey = generateKey();
|
|
99
99
|
const dupInColumns = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
|
|
@@ -125,7 +125,13 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
|
|
|
125
125
|
}
|
|
126
126
|
return item;
|
|
127
127
|
});
|
|
128
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
rows,
|
|
130
|
+
selectedBlockKey: newKey,
|
|
131
|
+
isDirty: true,
|
|
132
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
133
|
+
_future: [],
|
|
134
|
+
};
|
|
129
135
|
});
|
|
130
136
|
},
|
|
131
137
|
|
|
@@ -148,7 +154,6 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
|
|
|
148
154
|
},
|
|
149
155
|
|
|
150
156
|
reorderBlocks: (sectionKey: string, columnKey: string, fromIndex: number, toIndex: number): void => {
|
|
151
|
-
get()._pushSnapshot();
|
|
152
157
|
set((state) => {
|
|
153
158
|
const path = findSectionPath(state.rows, sectionKey);
|
|
154
159
|
if (!path) return state;
|
|
@@ -162,7 +167,12 @@ export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActi
|
|
|
162
167
|
return { ...c, blocks };
|
|
163
168
|
}),
|
|
164
169
|
}));
|
|
165
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
rows,
|
|
172
|
+
isDirty: true,
|
|
173
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
174
|
+
_future: [],
|
|
175
|
+
};
|
|
166
176
|
});
|
|
167
177
|
},
|
|
168
178
|
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
import { isCoverSection } from "../../lib/sanity/types";
|
|
20
20
|
import { generateKey } from "./utils";
|
|
21
21
|
import { createDefaultCoverSection, createDefaultCoverRow } from "./defaults";
|
|
22
|
+
import { pushSnapshot } from "./history";
|
|
22
23
|
|
|
23
24
|
type StoreSet = (
|
|
24
25
|
partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
|
|
@@ -82,7 +83,6 @@ function updateCoverInRows(
|
|
|
82
83
|
export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActions {
|
|
83
84
|
return {
|
|
84
85
|
addCoverSection: (afterRowKey?: string | null): void => {
|
|
85
|
-
get()._pushSnapshot();
|
|
86
86
|
const section = createDefaultCoverSection();
|
|
87
87
|
set((state) => {
|
|
88
88
|
const rows = [...state.rows];
|
|
@@ -96,12 +96,17 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
|
|
|
96
96
|
} else {
|
|
97
97
|
rows.push(section);
|
|
98
98
|
}
|
|
99
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
rows,
|
|
101
|
+
isDirty: true,
|
|
102
|
+
selectedRowKey: section._key,
|
|
103
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
104
|
+
_future: [],
|
|
105
|
+
};
|
|
100
106
|
});
|
|
101
107
|
},
|
|
102
108
|
|
|
103
109
|
addCoverRow: (sectionKey: string): void => {
|
|
104
|
-
get()._pushSnapshot();
|
|
105
110
|
set((state) => ({
|
|
106
111
|
rows: updateCoverInRows(state.rows, sectionKey, (section) => {
|
|
107
112
|
if (section.cover_rows.length >= 5) return section;
|
|
@@ -137,11 +142,12 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
|
|
|
137
142
|
return { ...section, cover_rows: newRows };
|
|
138
143
|
}),
|
|
139
144
|
isDirty: true,
|
|
145
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
146
|
+
_future: [],
|
|
140
147
|
}));
|
|
141
148
|
},
|
|
142
149
|
|
|
143
150
|
removeCoverRow: (sectionKey: string, rowKey: string): void => {
|
|
144
|
-
get()._pushSnapshot();
|
|
145
151
|
set((state) => ({
|
|
146
152
|
rows: updateCoverInRows(state.rows, sectionKey, (section) => {
|
|
147
153
|
if (section.cover_rows.length <= 1) return section;
|
|
@@ -183,6 +189,8 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
|
|
|
183
189
|
};
|
|
184
190
|
}),
|
|
185
191
|
isDirty: true,
|
|
192
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
193
|
+
_future: [],
|
|
186
194
|
}));
|
|
187
195
|
},
|
|
188
196
|
|
|
@@ -248,13 +256,14 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
|
|
|
248
256
|
"nav_color"
|
|
249
257
|
>>
|
|
250
258
|
): void => {
|
|
251
|
-
get()._pushSnapshot();
|
|
252
259
|
set((state) => ({
|
|
253
260
|
rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
|
|
254
261
|
...section,
|
|
255
262
|
...fields,
|
|
256
263
|
})),
|
|
257
264
|
isDirty: true,
|
|
265
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
266
|
+
_future: [],
|
|
258
267
|
}));
|
|
259
268
|
},
|
|
260
269
|
|
|
@@ -275,13 +284,14 @@ export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActi
|
|
|
275
284
|
sectionKey: string,
|
|
276
285
|
height: CoverSection["height"]
|
|
277
286
|
): void => {
|
|
278
|
-
get()._pushSnapshot();
|
|
279
287
|
set((state) => ({
|
|
280
288
|
rows: updateCoverInRows(state.rows, sectionKey, (section) => ({
|
|
281
289
|
...section,
|
|
282
290
|
height,
|
|
283
291
|
})),
|
|
284
292
|
isDirty: true,
|
|
293
|
+
_history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
|
|
294
|
+
_future: [],
|
|
285
295
|
}));
|
|
286
296
|
},
|
|
287
297
|
};
|