@morphika/andami 0.1.9 → 0.2.0
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/app/admin/pages/[slug]/page.tsx +3 -7
- package/app/api/admin/pages/[slug]/route.ts +2 -28
- package/app/api/admin/settings/route.ts +30 -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/EnterAnimationWrapper.tsx +19 -4
- package/components/blocks/PageRenderer.tsx +2 -15
- package/components/blocks/ProjectGridBlockRenderer.tsx +34 -36
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/DndWrapper.tsx +2 -24
- package/components/builder/InsertionLines.tsx +5 -5
- package/components/builder/ReadOnlyFrame.tsx +5 -49
- package/components/builder/SectionV2Canvas.tsx +2 -2
- package/components/builder/SectionV2Column.tsx +5 -5
- package/components/builder/SettingsPanel.tsx +0 -12
- package/components/builder/SortableBlock.tsx +3 -3
- package/components/builder/SortableRow.tsx +6 -27
- 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/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -3
- package/components/builder/live-preview/drag-utils.tsx +2 -2
- package/components/builder/settings-panel/AnimationTab.tsx +2 -16
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- 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 +0 -1
- package/components/builder/settings-panel/responsive-helpers.ts +2 -50
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/constants.ts +5 -4
- package/lib/builder/serializer/normalizers.ts +2 -40
- package/lib/builder/serializer/serializers.ts +3 -74
- package/lib/builder/store-blocks.ts +3 -19
- package/lib/builder/store-helpers.ts +2 -2
- package/lib/builder/store-sections.ts +26 -64
- package/lib/builder/store.ts +3 -6
- package/lib/builder/templates.ts +9 -45
- package/lib/builder/types.ts +4 -11
- package/lib/sanity/queries.ts +6 -29
- package/lib/sanity/types.ts +24 -70
- package/package.json +4 -1
- package/sanity/schemas/index.ts +0 -5
- package/sanity/schemas/objects/parallaxGroup.ts +2 -2
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +1 -0
- package/sanity/schemas/siteSettings.ts +42 -0
- package/styles/base.css +8 -2
- package/components/blocks/SectionRenderer.tsx +0 -171
- package/components/builder/settings-panel/LayoutTab.tsx +0 -382
- package/sanity/schemas/pageSection.ts +0 -157
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
SortableContext,
|
|
19
19
|
verticalListSortingStrategy,
|
|
20
20
|
} from "@dnd-kit/sortable";
|
|
21
|
-
import type { Page,
|
|
22
|
-
import {
|
|
21
|
+
import type { Page, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
|
|
22
|
+
import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } from "../../../../lib/sanity/types";
|
|
23
23
|
import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
|
|
24
24
|
import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
|
|
25
25
|
import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
|
|
@@ -665,10 +665,8 @@ export default function PageEditorPage() {
|
|
|
665
665
|
>
|
|
666
666
|
{store.rows.map((item, rowIndex) => {
|
|
667
667
|
const isV2Section = isPageSectionV2(item);
|
|
668
|
-
const isSection = isPageSection(item);
|
|
669
668
|
const isInstance = isCustomSectionInstance(item);
|
|
670
|
-
const isParallax =
|
|
671
|
-
const section = isSection ? (item as PageSection) : null;
|
|
669
|
+
const isParallax = isParallexGroup(item);
|
|
672
670
|
const v2Section = isV2Section ? (item as PageSectionV2) : null;
|
|
673
671
|
|
|
674
672
|
// Custom Section Instance — rendered directly without SortableRow chrome
|
|
@@ -770,8 +768,6 @@ export default function PageEditorPage() {
|
|
|
770
768
|
section={v2Section}
|
|
771
769
|
onAddBlockTarget={handleAddBlockTargetV2}
|
|
772
770
|
/>
|
|
773
|
-
) : isSection && section?.block?.[0] ? (
|
|
774
|
-
<BlockLivePreview block={section.block[0]} viewport={store.activeViewport} />
|
|
775
771
|
) : null}
|
|
776
772
|
</SortableRow>
|
|
777
773
|
);
|
|
@@ -18,7 +18,7 @@ interface RawNavItem {
|
|
|
18
18
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
20
|
/** Validate basic row→column→block structure before deeper sanitization.
|
|
21
|
-
* Supports
|
|
21
|
+
* Supports PageSectionV2, ParallaxGroup, and CustomSectionInstance.
|
|
22
22
|
*/
|
|
23
23
|
function validateBlockStructure(rows: unknown[]): { valid: boolean; error?: string } {
|
|
24
24
|
if (!Array.isArray(rows)) return { valid: false, error: "content_rows must be an array" };
|
|
@@ -43,20 +43,6 @@ function validateBlockStructure(rows: unknown[]): { valid: boolean; error?: stri
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// PageSection: validate block array instead of columns
|
|
47
|
-
if (r._type === "pageSection") {
|
|
48
|
-
if (!Array.isArray(r.block)) return { valid: false, error: `Section ${i}: block must be an array` };
|
|
49
|
-
const blocks = r.block as unknown[];
|
|
50
|
-
for (let k = 0; k < blocks.length; k++) {
|
|
51
|
-
const block = blocks[k];
|
|
52
|
-
if (!block || typeof block !== "object") return { valid: false, error: `Section ${i}, block ${k}: must be an object` };
|
|
53
|
-
const b = block as Record<string, unknown>;
|
|
54
|
-
if (typeof b._key !== "string" || !b._key) return { valid: false, error: `Section ${i}, block ${k}: missing _key` };
|
|
55
|
-
if (typeof b._type !== "string" || !b._type) return { valid: false, error: `Section ${i}, block ${k}: missing _type` };
|
|
56
|
-
}
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
46
|
// ParallaxGroup: validate slides array with columns→blocks inside each slide (Session 127)
|
|
61
47
|
if (r._type === "parallaxGroup") {
|
|
62
48
|
if (!Array.isArray(r.slides)) return { valid: false, error: `ParallaxGroup ${i}: slides must be an array` };
|
|
@@ -167,7 +153,7 @@ function sanitizeColumnsBlocks(columns: unknown[]): { valid: boolean; error?: st
|
|
|
167
153
|
}
|
|
168
154
|
|
|
169
155
|
/** Recursively sanitize URLs and asset paths in block content.
|
|
170
|
-
* Supports
|
|
156
|
+
* Supports PageSectionV2 (columns→blocks) and ParallaxGroup (slides→columns→blocks).
|
|
171
157
|
*/
|
|
172
158
|
function sanitizeBlockContent(rows: unknown[]): { valid: boolean; error?: string } {
|
|
173
159
|
if (!Array.isArray(rows)) return { valid: true };
|
|
@@ -176,18 +162,6 @@ function sanitizeBlockContent(rows: unknown[]): { valid: boolean; error?: string
|
|
|
176
162
|
if (!row || typeof row !== "object") continue;
|
|
177
163
|
const rowRecord = row as Record<string, unknown>;
|
|
178
164
|
|
|
179
|
-
// PageSection: sanitize block array directly
|
|
180
|
-
if (rowRecord._type === "pageSection") {
|
|
181
|
-
const sectionBlocks = rowRecord.block;
|
|
182
|
-
if (!Array.isArray(sectionBlocks)) continue;
|
|
183
|
-
for (const block of sectionBlocks) {
|
|
184
|
-
if (!block || typeof block !== "object") continue;
|
|
185
|
-
const result = sanitizeSingleBlock(block as Record<string, unknown>);
|
|
186
|
-
if (!result.valid) return result;
|
|
187
|
-
}
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
165
|
// ParallaxGroup: sanitize blocks inside each slide's columns (Session 127)
|
|
192
166
|
if (rowRecord._type === "parallaxGroup") {
|
|
193
167
|
const slides = rowRecord.slides;
|
|
@@ -16,6 +16,23 @@ import { logger } from "../../../../lib/logger";
|
|
|
16
16
|
|
|
17
17
|
const SETTINGS_ID = "siteSettings";
|
|
18
18
|
|
|
19
|
+
/** Sanitize a per-viewport responsive override object for nav_design.
|
|
20
|
+
* Only allows the known overridable fields with proper type/range checks. */
|
|
21
|
+
function sanitizeNavResponsiveOverride(o: Record<string, unknown>) {
|
|
22
|
+
const result: Record<string, unknown> = {};
|
|
23
|
+
if (typeof o.font_size === "number") result.font_size = Math.max(8, Math.min(48, o.font_size));
|
|
24
|
+
if (typeof o.font_weight === "string") result.font_weight = o.font_weight.slice(0, 10);
|
|
25
|
+
if (typeof o.text_align === "string" && ["left", "center", "right"].includes(o.text_align)) result.text_align = o.text_align;
|
|
26
|
+
if (typeof o.vertical_align === "string" && ["top", "middle", "bottom"].includes(o.vertical_align)) result.vertical_align = o.vertical_align;
|
|
27
|
+
if (typeof o.text_transform === "string" && ["none", "uppercase", "lowercase", "capitalize"].includes(o.text_transform)) result.text_transform = o.text_transform;
|
|
28
|
+
if (typeof o.padding_h === "number") result.padding_h = Math.max(0, Math.min(200, o.padding_h));
|
|
29
|
+
if (typeof o.padding_v === "number") result.padding_v = Math.max(0, Math.min(200, o.padding_v));
|
|
30
|
+
if (typeof o.margin_h === "number") result.margin_h = Math.max(0, Math.min(200, o.margin_h));
|
|
31
|
+
if (typeof o.margin_v === "number") result.margin_v = Math.max(0, Math.min(200, o.margin_v));
|
|
32
|
+
if (typeof o.items_gap === "number") result.items_gap = Math.max(0, Math.min(200, o.items_gap));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
export async function GET() {
|
|
20
37
|
const authenticated = await isAdminAuthenticated();
|
|
21
38
|
if (!authenticated) {
|
|
@@ -221,6 +238,19 @@ export async function POST(request: NextRequest) {
|
|
|
221
238
|
entrance_delay: typeof nd.entrance_delay === "number" ? Math.max(0, Math.min(5000, nd.entrance_delay)) : 0,
|
|
222
239
|
entrance_stagger: !!nd.entrance_stagger,
|
|
223
240
|
entrance_stagger_delay: typeof nd.entrance_stagger_delay === "number" ? Math.max(20, Math.min(500, nd.entrance_stagger_delay)) : 80,
|
|
241
|
+
// ── Responsive overrides (tablet / phone) ──
|
|
242
|
+
...(nd.responsive && typeof nd.responsive === "object"
|
|
243
|
+
? {
|
|
244
|
+
responsive: {
|
|
245
|
+
...(nd.responsive.tablet && typeof nd.responsive.tablet === "object"
|
|
246
|
+
? { tablet: sanitizeNavResponsiveOverride(nd.responsive.tablet) }
|
|
247
|
+
: {}),
|
|
248
|
+
...(nd.responsive.phone && typeof nd.responsive.phone === "object"
|
|
249
|
+
? { phone: sanitizeNavResponsiveOverride(nd.responsive.phone) }
|
|
250
|
+
: {}),
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
224
254
|
},
|
|
225
255
|
};
|
|
226
256
|
break;
|
|
@@ -7,6 +7,44 @@ import NavBuilderGrid from "./NavBuilderGrid";
|
|
|
7
7
|
import NavLivePreview from "./NavLivePreview";
|
|
8
8
|
import NavSettingsPanel from "./NavSettingsPanel";
|
|
9
9
|
|
|
10
|
+
// ── Header icons ──
|
|
11
|
+
|
|
12
|
+
const NAV_HEADER_ICON = (
|
|
13
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,0,0,0.55)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
14
|
+
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
const LOGO_HEADER_ICON = (
|
|
18
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,0,0,0.55)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
19
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
const ITEM_HEADER_ICON = (
|
|
23
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="rgba(0,0,0,0.55)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
24
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
25
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function getHeaderConfig(selectedItem: NavItem | null) {
|
|
30
|
+
if (!selectedItem) {
|
|
31
|
+
return {
|
|
32
|
+
gradient: "linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 40%, #ddd6fe 100%)",
|
|
33
|
+
icon: NAV_HEADER_ICON,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (selectedItem.type === "logo") {
|
|
37
|
+
return {
|
|
38
|
+
gradient: "linear-gradient(135deg, #fef3c7 0%, #fde68a 40%, #fcd34d 100%)",
|
|
39
|
+
icon: LOGO_HEADER_ICON,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
gradient: "linear-gradient(135deg, #dbeafe 0%, #bfdbfe 40%, #c7d2fe 100%)",
|
|
44
|
+
icon: ITEM_HEADER_ICON,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
interface NavBuilderProps {
|
|
11
49
|
initialItems: NavItem[];
|
|
12
50
|
initialDesign: NavDesign;
|
|
@@ -121,30 +159,68 @@ export default function NavBuilder({
|
|
|
121
159
|
}, [items, design, onSave]);
|
|
122
160
|
|
|
123
161
|
const selectedItem = items.find((i) => i._key === selectedKey) || null;
|
|
162
|
+
const headerConfig = getHeaderConfig(selectedItem);
|
|
163
|
+
|
|
164
|
+
const isItemMode = !!selectedItem;
|
|
165
|
+
const headerTitle = isItemMode
|
|
166
|
+
? selectedItem.type === "logo"
|
|
167
|
+
? "Logo"
|
|
168
|
+
: selectedItem.label || "Untitled"
|
|
169
|
+
: "Navigation";
|
|
170
|
+
const headerSubtitle = isItemMode
|
|
171
|
+
? `${selectedItem.type === "logo" ? "Logo" : "Menu Item"} · Col ${selectedItem.grid_column}${
|
|
172
|
+
selectedItem.column_span > 1
|
|
173
|
+
? `–${selectedItem.grid_column + selectedItem.column_span - 1}`
|
|
174
|
+
: ""
|
|
175
|
+
}`
|
|
176
|
+
: `${items.length} items · 12 columns`;
|
|
124
177
|
|
|
125
178
|
return (
|
|
126
179
|
<div className="bg-white rounded-2xl overflow-hidden border border-neutral-200">
|
|
127
|
-
{/*
|
|
128
|
-
<div
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
{/* ── Gradient header (matches Mobile Menu style) ── */}
|
|
181
|
+
<div
|
|
182
|
+
className="relative flex items-center px-4 py-3.5 overflow-hidden"
|
|
183
|
+
style={{ background: headerConfig.gradient }}
|
|
184
|
+
>
|
|
185
|
+
<div
|
|
186
|
+
className="absolute inset-0 pointer-events-none"
|
|
187
|
+
style={{ background: "linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.05) 100%)" }}
|
|
188
|
+
/>
|
|
189
|
+
<div
|
|
190
|
+
className="relative shrink-0 flex items-center justify-center"
|
|
191
|
+
style={{
|
|
192
|
+
width: 34,
|
|
193
|
+
height: 34,
|
|
194
|
+
borderRadius: 10,
|
|
195
|
+
background: "rgba(255,255,255,0.4)",
|
|
196
|
+
backdropFilter: "blur(8px)",
|
|
197
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.5)",
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{headerConfig.icon}
|
|
201
|
+
</div>
|
|
202
|
+
<div className="relative z-10 ml-2.5 min-w-0 flex-1">
|
|
203
|
+
<h3
|
|
204
|
+
className="text-[13px] font-semibold truncate"
|
|
205
|
+
style={{ color: "rgba(0,0,0,0.72)", textShadow: "0 1px 0 rgba(255,255,255,0.3)" }}
|
|
206
|
+
>
|
|
207
|
+
{headerTitle}
|
|
208
|
+
</h3>
|
|
209
|
+
<p className="text-[10px] mt-0.5" style={{ color: "rgba(0,0,0,0.38)" }}>
|
|
210
|
+
{headerSubtitle}
|
|
211
|
+
</p>
|
|
134
212
|
</div>
|
|
135
|
-
<div className="flex items-center gap-3">
|
|
213
|
+
<div className="relative z-10 flex items-center gap-3">
|
|
136
214
|
{hasChanges && (
|
|
137
|
-
<span className="text-[11px] text-amber-
|
|
138
|
-
Unsaved changes
|
|
139
|
-
</span>
|
|
215
|
+
<span className="text-[11px] text-amber-600 font-medium">Unsaved</span>
|
|
140
216
|
)}
|
|
141
217
|
<button
|
|
142
218
|
onClick={handleSave}
|
|
143
219
|
disabled={saving || !hasChanges}
|
|
144
|
-
className={`px-
|
|
220
|
+
className={`px-4 py-1.5 text-[11px] font-medium rounded-lg transition-all ${
|
|
145
221
|
saving || !hasChanges
|
|
146
|
-
? "bg-
|
|
147
|
-
: "bg-[#076bff]
|
|
222
|
+
? "bg-white/40 text-neutral-400 cursor-not-allowed"
|
|
223
|
+
: "bg-white text-[#076bff] shadow-sm hover:shadow-md"
|
|
148
224
|
}`}
|
|
149
225
|
>
|
|
150
226
|
{saving ? "Saving..." : "Save"}
|