@morphika/andami 0.1.3 → 0.1.5
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/(site)/[slug]/page.tsx +2 -2
- package/app/(site)/layout.tsx +1 -0
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +2 -2
- package/app/admin/layout.tsx +2 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/settings/route.ts +40 -15
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +6 -3
- package/components/admin/index.ts +7 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
- package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
- package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
- package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
- package/components/admin/nav-builder/index.ts +2 -0
- package/components/blocks/BlockRenderer.tsx +65 -13
- package/components/blocks/ButtonBlockRenderer.tsx +29 -6
- package/components/blocks/CoverBlockRenderer.tsx +36 -14
- package/components/blocks/ImageBlockRenderer.tsx +5 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
- package/components/blocks/PageRenderer.tsx +4 -2
- package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
- package/components/blocks/SectionRenderer.tsx +9 -8
- package/components/blocks/SectionV2Renderer.tsx +8 -8
- package/components/blocks/SpacerBlockRenderer.tsx +4 -2
- package/components/blocks/TextBlockRenderer.tsx +9 -4
- package/components/builder/BuilderCanvas.tsx +10 -4
- package/components/builder/ColorPicker.tsx +51 -243
- package/components/builder/ColorSwatchPicker.tsx +214 -274
- package/components/builder/DndWrapper.tsx +5 -2
- package/components/builder/SectionV2Canvas.tsx +15 -4
- package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
- package/components/builder/color-picker/AlphaSlider.tsx +141 -0
- package/components/builder/color-picker/AngleControl.tsx +138 -0
- package/components/builder/color-picker/ColorInputs.tsx +105 -0
- package/components/builder/color-picker/EyedropperButton.tsx +74 -0
- package/components/builder/color-picker/GradientBar.tsx +222 -0
- package/components/builder/color-picker/GradientPreview.tsx +53 -0
- package/components/builder/color-picker/HueSlider.tsx +124 -0
- package/components/builder/color-picker/MeshCanvas.tsx +172 -0
- package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
- package/components/builder/color-picker/MeshPointList.tsx +200 -0
- package/components/builder/color-picker/PositionControl.tsx +158 -0
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
- package/components/builder/color-picker/StopEditor.tsx +178 -0
- package/components/builder/color-picker/SwatchBar.tsx +93 -0
- package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
- package/components/builder/color-picker/index.ts +62 -0
- package/components/builder/color-picker/types.ts +115 -0
- package/components/builder/color-picker/utils.ts +138 -0
- package/components/builder/editors/CoverBlockEditor.tsx +86 -32
- package/components/builder/editors/ProjectGridEditor.tsx +51 -4
- package/components/builder/hooks/useColumnDrag.ts +25 -27
- package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
- package/components/builder/settings-panel/LayoutTab.tsx +382 -310
- package/components/builder/settings-panel/PageSettings.tsx +6 -4
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
- package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
- package/components/ui/Navbar.tsx +95 -25
- package/components/ui/PortfolioTracker.tsx +3 -3
- package/lib/assets.ts +1 -1
- package/lib/auth.ts +1 -1
- package/lib/builder/gradient-presets.ts +128 -0
- package/lib/builder/layout-styles.ts +16 -10
- package/lib/builder/serializer.ts +1 -0
- package/lib/builder/store-blocks.ts +48 -61
- package/lib/builder/store-helpers.ts +31 -14
- package/lib/builder/store.ts +59 -41
- package/lib/builder/types.ts +14 -0
- package/lib/color-utils.ts +200 -0
- package/lib/revalidate.ts +2 -2
- package/lib/sanity/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- package/package.json +8 -2
- package/sanity/schemas/siteSettings.ts +34 -0
- package/styles/base.css +3 -3
- package/app/globals.css +0 -7
|
@@ -15,7 +15,6 @@ import { logger } from "../../../../lib/logger";
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const SETTINGS_ID = "siteSettings";
|
|
18
|
-
const cfg = getSiteConfig();
|
|
19
18
|
|
|
20
19
|
export async function GET() {
|
|
21
20
|
const authenticated = await isAdminAuthenticated();
|
|
@@ -32,7 +31,7 @@ export async function GET() {
|
|
|
32
31
|
_id: SETTINGS_ID,
|
|
33
32
|
_type: "siteSettings",
|
|
34
33
|
nav_items: [],
|
|
35
|
-
default_title:
|
|
34
|
+
default_title: getSiteConfig().defaults.metaTitle,
|
|
36
35
|
});
|
|
37
36
|
settings = await client.fetch(siteSettingsQuery);
|
|
38
37
|
}
|
|
@@ -71,7 +70,7 @@ export async function POST(request: NextRequest) {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
// Validate section name
|
|
74
|
-
const validSections = ["navigation", "nav_design", "metadata", "assets"];
|
|
73
|
+
const validSections = ["navigation", "nav_design", "nav_mobile_design", "metadata", "assets"];
|
|
75
74
|
if (!validSections.includes(section)) {
|
|
76
75
|
return NextResponse.json(
|
|
77
76
|
{ error: "Invalid section" },
|
|
@@ -111,14 +110,17 @@ export async function POST(request: NextRequest) {
|
|
|
111
110
|
{ status: 400 }
|
|
112
111
|
);
|
|
113
112
|
}
|
|
114
|
-
|
|
113
|
+
// Logo items don't require a link_type — default to "internal" if missing
|
|
114
|
+
const isLogo = item.type === "logo";
|
|
115
|
+
const linkType = item.link_type || (isLogo ? "internal" : undefined);
|
|
116
|
+
if (!linkType || !["internal", "external", "content"].includes(linkType)) {
|
|
115
117
|
return NextResponse.json(
|
|
116
|
-
{ error:
|
|
118
|
+
{ error: `link_type must be 'internal', 'external', or 'content' (item: "${item.label}")` },
|
|
117
119
|
{ status: 400 }
|
|
118
120
|
);
|
|
119
121
|
}
|
|
120
122
|
// Validate external URLs — reject javascript:, data:, vbscript: etc.
|
|
121
|
-
if (
|
|
123
|
+
if (linkType === "external" && item.external_url) {
|
|
122
124
|
if (!isSafeUrl(item.external_url)) {
|
|
123
125
|
return NextResponse.json(
|
|
124
126
|
{ error: "External URL uses a disallowed protocol" },
|
|
@@ -127,7 +129,7 @@ export async function POST(request: NextRequest) {
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
// Validate content embed URLs
|
|
130
|
-
if (
|
|
132
|
+
if (linkType === "content" && item.content_type === "video-embed" && item.content_url) {
|
|
131
133
|
if (!isSafeUrl(item.content_url)) {
|
|
132
134
|
return NextResponse.json(
|
|
133
135
|
{ error: "Content embed URL uses a disallowed protocol" },
|
|
@@ -143,7 +145,7 @@ export async function POST(request: NextRequest) {
|
|
|
143
145
|
type?: string;
|
|
144
146
|
label: string;
|
|
145
147
|
logo_image?: string;
|
|
146
|
-
link_type
|
|
148
|
+
link_type?: string;
|
|
147
149
|
internal_page?: { _ref: string };
|
|
148
150
|
external_url?: string;
|
|
149
151
|
content_type?: string;
|
|
@@ -159,14 +161,14 @@ export async function POST(request: NextRequest) {
|
|
|
159
161
|
type: item.type || "menu-item",
|
|
160
162
|
label: item.label,
|
|
161
163
|
...(item.logo_image ? { logo_image: item.logo_image } : {}),
|
|
162
|
-
link_type: item.link_type,
|
|
163
|
-
...(item.link_type === "internal" && item.internal_page
|
|
164
|
+
link_type: item.link_type || (item.type === "logo" ? "internal" : item.link_type),
|
|
165
|
+
...((item.link_type || "internal") === "internal" && item.internal_page
|
|
164
166
|
? { internal_page: { _type: "reference", _ref: item.internal_page._ref } }
|
|
165
167
|
: {}),
|
|
166
|
-
...(item.link_type === "external" && item.external_url
|
|
168
|
+
...((item.link_type || "internal") === "external" && item.external_url
|
|
167
169
|
? { external_url: item.external_url }
|
|
168
170
|
: {}),
|
|
169
|
-
...(item.link_type === "content" ? {
|
|
171
|
+
...((item.link_type || "internal") === "content" ? {
|
|
170
172
|
...(item.content_type ? { content_type: item.content_type } : {}),
|
|
171
173
|
...(item.content_asset ? { content_asset: item.content_asset } : {}),
|
|
172
174
|
...(item.content_url ? { content_url: item.content_url } : {}),
|
|
@@ -194,8 +196,8 @@ export async function POST(request: NextRequest) {
|
|
|
194
196
|
patch = {
|
|
195
197
|
nav_design: {
|
|
196
198
|
_type: "object",
|
|
197
|
-
logo_text: typeof nd.logo_text === "string" ? nd.logo_text.slice(0, 200) :
|
|
198
|
-
color: ["yellow-lime", "yellow", "red-coral", "blue", "green", "white"].includes(nd.color) ? nd.color : "yellow-lime",
|
|
199
|
+
logo_text: typeof nd.logo_text === "string" ? nd.logo_text.slice(0, 200) : getSiteConfig().defaults.logoText,
|
|
200
|
+
color: typeof nd.color === "string" && (["yellow-lime", "yellow", "red-coral", "blue", "green", "white"].includes(nd.color) || /^#[0-9a-fA-F]{6}$/.test(nd.color)) ? nd.color : "yellow-lime",
|
|
199
201
|
position: ["fixed", "sticky", "static"].includes(nd.position) ? nd.position : "fixed",
|
|
200
202
|
hide_on_scroll: nd.hide_on_scroll !== false,
|
|
201
203
|
font_size: typeof nd.font_size === "number" ? Math.max(8, Math.min(48, nd.font_size)) : 14,
|
|
@@ -206,7 +208,7 @@ export async function POST(request: NextRequest) {
|
|
|
206
208
|
padding_v: typeof nd.padding_v === "number" ? Math.max(0, Math.min(200, nd.padding_v)) : 27,
|
|
207
209
|
margin_h: typeof nd.margin_h === "number" ? Math.max(0, Math.min(200, nd.margin_h)) : 0,
|
|
208
210
|
margin_v: typeof nd.margin_v === "number" ? Math.max(0, Math.min(200, nd.margin_v)) : 0,
|
|
209
|
-
background_color: typeof nd.background_color === "string" ? nd.background_color.slice(0,
|
|
211
|
+
background_color: typeof nd.background_color === "string" ? nd.background_color.slice(0, 500) : "",
|
|
210
212
|
background_opacity: typeof nd.background_opacity === "number" ? Math.max(0, Math.min(100, nd.background_opacity)) : 0,
|
|
211
213
|
backdrop_blur: !!nd.backdrop_blur,
|
|
212
214
|
items_gap: typeof nd.items_gap === "number" ? Math.max(0, Math.min(200, nd.items_gap)) : 32,
|
|
@@ -224,6 +226,29 @@ export async function POST(request: NextRequest) {
|
|
|
224
226
|
break;
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
case "nav_mobile_design": {
|
|
230
|
+
const md = data.nav_mobile_design || {};
|
|
231
|
+
patch = {
|
|
232
|
+
nav_mobile_design: {
|
|
233
|
+
_type: "object",
|
|
234
|
+
// Overlay
|
|
235
|
+
overlay_bg: typeof md.overlay_bg === "string" ? md.overlay_bg.slice(0, 50) : "",
|
|
236
|
+
text_color: typeof md.text_color === "string" ? md.text_color.slice(0, 50) : "",
|
|
237
|
+
font_size: typeof md.font_size === "number" ? Math.max(12, Math.min(72, md.font_size)) : 24,
|
|
238
|
+
text_transform: ["none", "uppercase", "lowercase", "capitalize"].includes(md.text_transform) ? md.text_transform : "uppercase",
|
|
239
|
+
items_gap: typeof md.items_gap === "number" ? Math.max(0, Math.min(120, md.items_gap)) : 32,
|
|
240
|
+
items_align: ["left", "center", "right"].includes(md.items_align) ? md.items_align : "center",
|
|
241
|
+
// Navbar bar
|
|
242
|
+
navbar_bg: typeof md.navbar_bg === "string" ? md.navbar_bg.slice(0, 50) : "",
|
|
243
|
+
navbar_bg_opacity: typeof md.navbar_bg_opacity === "number" ? Math.max(0, Math.min(100, md.navbar_bg_opacity)) : 0,
|
|
244
|
+
hamburger_color: typeof md.hamburger_color === "string" ? md.hamburger_color.slice(0, 50) : "",
|
|
245
|
+
padding_h: typeof md.padding_h === "number" ? Math.max(0, Math.min(120, md.padding_h)) : 24,
|
|
246
|
+
padding_v: typeof md.padding_v === "number" ? Math.max(0, Math.min(120, md.padding_v)) : 27,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
227
252
|
case "metadata": {
|
|
228
253
|
// Validate text field lengths
|
|
229
254
|
if (data.default_title && typeof data.default_title === "string" && data.default_title.length > 1000) {
|
|
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
|
|
|
26
26
|
try {
|
|
27
27
|
// Find the siteSettings document
|
|
28
28
|
const doc = await client.fetch<{ _id: string } | null>(
|
|
29
|
-
`*[
|
|
29
|
+
`*[_id == "siteSettings"][0]{ _id }`
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
if (!doc) {
|
|
@@ -59,7 +59,8 @@ export async function POST(request: NextRequest) {
|
|
|
59
59
|
|
|
60
60
|
// Seed siteSettings
|
|
61
61
|
if (existing.siteSettings === 0) {
|
|
62
|
-
await writeClient.
|
|
62
|
+
await writeClient.createIfNotExists({
|
|
63
|
+
_id: "siteSettings",
|
|
63
64
|
_type: "siteSettings",
|
|
64
65
|
site_title: "",
|
|
65
66
|
site_description: "",
|
|
@@ -84,7 +85,8 @@ export async function POST(request: NextRequest) {
|
|
|
84
85
|
|
|
85
86
|
// Seed siteStyles
|
|
86
87
|
if (existing.siteStyles === 0) {
|
|
87
|
-
await writeClient.
|
|
88
|
+
await writeClient.createIfNotExists({
|
|
89
|
+
_id: "siteStyles",
|
|
88
90
|
_type: "siteStyles",
|
|
89
91
|
grid_columns: 12,
|
|
90
92
|
grid_width: 1200,
|
|
@@ -100,7 +102,8 @@ export async function POST(request: NextRequest) {
|
|
|
100
102
|
|
|
101
103
|
// Seed assetRegistry
|
|
102
104
|
if (existing.assetRegistry === 0) {
|
|
103
|
-
await writeClient.
|
|
105
|
+
await writeClient.createIfNotExists({
|
|
106
|
+
_id: "assetRegistry",
|
|
104
107
|
_type: "assetRegistry",
|
|
105
108
|
assets: [],
|
|
106
109
|
storage_provider: "",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// Admin Components Barrel Export
|
|
3
|
+
// ============================================
|
|
4
|
+
|
|
5
|
+
export { default as MetadataEditor } from "./MetadataEditor";
|
|
6
|
+
export { default as PublishToggle } from "./PublishToggle";
|
|
7
|
+
export * from "./icons";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { NavDesign } from "../../../lib/sanity/types";
|
|
3
|
+
import type { NavDesign, ColorField } from "../../../lib/sanity/types";
|
|
4
4
|
import { getSiteConfig } from "../../../lib/config";
|
|
5
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../../builder/ColorSwatchPicker";
|
|
5
6
|
import {
|
|
6
7
|
Field,
|
|
7
8
|
TextInput,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
Toggle,
|
|
11
12
|
RangeSlider,
|
|
12
13
|
Section,
|
|
13
|
-
ColorInput,
|
|
14
14
|
} from "./NavSettingsFields";
|
|
15
15
|
|
|
16
16
|
interface NavGeneralSettingsProps {
|
|
@@ -26,6 +26,8 @@ export default function NavGeneralSettings({
|
|
|
26
26
|
onChange,
|
|
27
27
|
fonts,
|
|
28
28
|
}: NavGeneralSettingsProps) {
|
|
29
|
+
const swatches = usePaletteSwatches();
|
|
30
|
+
|
|
29
31
|
const update = (partial: Partial<NavDesign>) =>
|
|
30
32
|
onChange({ ...design, ...partial });
|
|
31
33
|
|
|
@@ -122,17 +124,10 @@ export default function NavGeneralSettings({
|
|
|
122
124
|
/>
|
|
123
125
|
</Field>
|
|
124
126
|
<Field label="Color">
|
|
125
|
-
<
|
|
126
|
-
value={design.color || "
|
|
127
|
-
onChange={(v) => update({ color: v
|
|
128
|
-
|
|
129
|
-
{ value: "yellow-lime", label: "Yellow-Lime" },
|
|
130
|
-
{ value: "yellow", label: "Yellow" },
|
|
131
|
-
{ value: "red-coral", label: "Red / Coral" },
|
|
132
|
-
{ value: "blue", label: "Blue" },
|
|
133
|
-
{ value: "green", label: "Green" },
|
|
134
|
-
{ value: "white", label: "White" },
|
|
135
|
-
]}
|
|
127
|
+
<ColorSwatchPicker
|
|
128
|
+
value={design.color || ""}
|
|
129
|
+
onChange={(v) => update({ color: typeof v === "string" ? v : "" })}
|
|
130
|
+
swatches={swatches}
|
|
136
131
|
/>
|
|
137
132
|
</Field>
|
|
138
133
|
</Section>
|
|
@@ -247,9 +242,10 @@ export default function NavGeneralSettings({
|
|
|
247
242
|
<>
|
|
248
243
|
<Section title="BACKGROUND">
|
|
249
244
|
<Field label="Color">
|
|
250
|
-
<
|
|
245
|
+
<ColorSwatchPicker
|
|
251
246
|
value={design.background_color || ""}
|
|
252
|
-
onChange={(v) => update({ background_color: v })}
|
|
247
|
+
onChange={(v) => update({ background_color: typeof v === "string" ? v : "" })}
|
|
248
|
+
swatches={swatches}
|
|
253
249
|
/>
|
|
254
250
|
</Field>
|
|
255
251
|
{design.background_color && (
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import type { NavItem, PageListItem } from "../../../lib/sanity/types";
|
|
5
5
|
import { TOTAL_COLUMNS, getMaxSpan } from "./nav-builder-utils";
|
|
6
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../../builder/ColorSwatchPicker";
|
|
6
7
|
import {
|
|
7
8
|
Field,
|
|
8
9
|
TextInput,
|
|
9
10
|
SelectInput,
|
|
10
11
|
SegmentedControl,
|
|
11
12
|
Section,
|
|
12
|
-
ColorInput,
|
|
13
13
|
} from "./NavSettingsFields";
|
|
14
14
|
import AssetBrowser from "../../../components/builder/AssetBrowser";
|
|
15
15
|
|
|
@@ -33,6 +33,8 @@ export default function NavItemSettings({
|
|
|
33
33
|
const isLogo = item.type === "logo";
|
|
34
34
|
const maxSpan = getMaxSpan(items, item.grid_column, item._key);
|
|
35
35
|
const [showAssetBrowser, setShowAssetBrowser] = useState(false);
|
|
36
|
+
const [showLogoAssetBrowser, setShowLogoAssetBrowser] = useState(false);
|
|
37
|
+
const swatches = usePaletteSwatches();
|
|
36
38
|
|
|
37
39
|
const update = (partial: Partial<NavItem>) =>
|
|
38
40
|
onUpdate({ ...item, ...partial });
|
|
@@ -60,7 +62,10 @@ export default function NavItemSettings({
|
|
|
60
62
|
</Field>
|
|
61
63
|
{isLogo && (
|
|
62
64
|
<Field label="Image">
|
|
63
|
-
<button
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => setShowLogoAssetBrowser(true)}
|
|
67
|
+
className="w-full py-4 px-2.5 bg-neutral-50 border border-dashed border-neutral-200 rounded-lg text-neutral-400 text-[11px] cursor-pointer flex flex-col items-center gap-1 hover:border-[#076bff]/40 hover:bg-[#076bff]/[0.02] transition-colors"
|
|
68
|
+
>
|
|
64
69
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
65
70
|
<rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
|
66
71
|
<circle cx="10" cy="7.5" r="1.5" stroke="currentColor" strokeWidth="1.2" />
|
|
@@ -74,6 +79,14 @@ export default function NavItemSettings({
|
|
|
74
79
|
"Browse image..."
|
|
75
80
|
)}
|
|
76
81
|
</button>
|
|
82
|
+
{item.logo_image && (
|
|
83
|
+
<button
|
|
84
|
+
onClick={() => update({ logo_image: undefined })}
|
|
85
|
+
className="mt-1 text-[10px] text-red-400 hover:text-red-600 transition-colors"
|
|
86
|
+
>
|
|
87
|
+
Remove image
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
77
90
|
</Field>
|
|
78
91
|
)}
|
|
79
92
|
</Section>
|
|
@@ -174,6 +187,17 @@ export default function NavItemSettings({
|
|
|
174
187
|
onClose={() => setShowAssetBrowser(false)}
|
|
175
188
|
filterType={item.content_type === "video-file" ? "video" : item.content_type === "image" ? "image" : "all"}
|
|
176
189
|
/>
|
|
190
|
+
|
|
191
|
+
{/* Logo image asset browser */}
|
|
192
|
+
<AssetBrowser
|
|
193
|
+
open={showLogoAssetBrowser}
|
|
194
|
+
onSelect={(path) => {
|
|
195
|
+
update({ logo_image: path });
|
|
196
|
+
setShowLogoAssetBrowser(false);
|
|
197
|
+
}}
|
|
198
|
+
onClose={() => setShowLogoAssetBrowser(false)}
|
|
199
|
+
filterType="image"
|
|
200
|
+
/>
|
|
177
201
|
</>
|
|
178
202
|
);
|
|
179
203
|
}
|
|
@@ -263,10 +287,10 @@ export default function NavItemSettings({
|
|
|
263
287
|
/>
|
|
264
288
|
</Field>
|
|
265
289
|
<Field label="Color">
|
|
266
|
-
<
|
|
290
|
+
<ColorSwatchPicker
|
|
267
291
|
value={item.style_overrides?.color || ""}
|
|
268
|
-
onChange={(v) => updateOverride("color", v || undefined)}
|
|
269
|
-
|
|
292
|
+
onChange={(v) => updateOverride("color", (typeof v === "string" ? v : "") || undefined)}
|
|
293
|
+
swatches={swatches}
|
|
270
294
|
/>
|
|
271
295
|
</Field>
|
|
272
296
|
<Field label="Transform">
|
|
@@ -41,7 +41,10 @@ export default function NavLivePreview({ items, design }: NavLivePreviewProps) {
|
|
|
41
41
|
: textAlign === "right"
|
|
42
42
|
? "flex-end"
|
|
43
43
|
: "flex-start";
|
|
44
|
-
const
|
|
44
|
+
const colorKey = design.color || "yellow-lime";
|
|
45
|
+
const color = /^#[0-9a-fA-F]{6}$/.test(colorKey)
|
|
46
|
+
? colorKey
|
|
47
|
+
: NAV_COLOR_HEX[colorKey as keyof typeof NAV_COLOR_HEX] || NAV_COLOR_HEX["yellow-lime"];
|
|
45
48
|
const verticalAlign = design.vertical_align || "top";
|
|
46
49
|
const gridAlignItems = verticalAlign === "bottom" ? "end" : verticalAlign === "middle" ? "center" : "start";
|
|
47
50
|
const hasMargin = marginH > 0 || marginV > 0;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { NavItem, NavDesign, MobileNavDesign, NavColorVariant } from "../../../lib/sanity/types";
|
|
4
|
+
import { hexToRgba } from "./nav-builder-utils";
|
|
5
|
+
import { getSiteConfig } from "../../../lib/config";
|
|
6
|
+
|
|
7
|
+
const _cfg = getSiteConfig();
|
|
8
|
+
|
|
9
|
+
const NAV_COLOR_HEX: Record<NavColorVariant, string> = {
|
|
10
|
+
"yellow-lime": _cfg.palette.accent,
|
|
11
|
+
yellow: _cfg.palette.accent,
|
|
12
|
+
"red-coral": _cfg.palette.secondary,
|
|
13
|
+
blue: "#076bff",
|
|
14
|
+
green: _cfg.palette.accent,
|
|
15
|
+
white: _cfg.palette.text,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// NavMobileLivePreview — phone-sized preview of the mobile menu
|
|
20
|
+
// Session 158: Mirrors NavLivePreview visual language (dark bg, footer label)
|
|
21
|
+
// but renders the mobile hamburger overlay layout inside a phone frame.
|
|
22
|
+
// Designed to sit on the right side of NavMobileSettings.
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
interface NavMobileLivePreviewProps {
|
|
26
|
+
items: NavItem[];
|
|
27
|
+
design: NavDesign;
|
|
28
|
+
mobileDesign: MobileNavDesign;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function NavMobileLivePreview({
|
|
32
|
+
items,
|
|
33
|
+
design,
|
|
34
|
+
mobileDesign,
|
|
35
|
+
}: NavMobileLivePreviewProps) {
|
|
36
|
+
// Desktop fallback values
|
|
37
|
+
const desktopColorKey = design.color || "yellow-lime";
|
|
38
|
+
const desktopColor = /^#[0-9a-fA-F]{6}$/.test(desktopColorKey)
|
|
39
|
+
? desktopColorKey
|
|
40
|
+
: NAV_COLOR_HEX[desktopColorKey as keyof typeof NAV_COLOR_HEX] || NAV_COLOR_HEX["yellow-lime"];
|
|
41
|
+
|
|
42
|
+
// ── Navbar bar (logo + hamburger) ──
|
|
43
|
+
const barPaddingH = mobileDesign.padding_h ?? design.padding_h ?? 24;
|
|
44
|
+
const barPaddingV = mobileDesign.padding_v ?? design.padding_v ?? 27;
|
|
45
|
+
const barBg = mobileDesign.navbar_bg || "";
|
|
46
|
+
const barBgOpacity = mobileDesign.navbar_bg_opacity ?? 0;
|
|
47
|
+
const barBgColor =
|
|
48
|
+
barBg && barBgOpacity > 0 ? hexToRgba(barBg, barBgOpacity / 100) : "transparent";
|
|
49
|
+
const hamburgerColor = mobileDesign.hamburger_color || desktopColor;
|
|
50
|
+
|
|
51
|
+
// ── Overlay (expanded menu) ──
|
|
52
|
+
const overlayBg = mobileDesign.overlay_bg || "#0a0a0a";
|
|
53
|
+
const textColor = mobileDesign.text_color || desktopColor;
|
|
54
|
+
const fontSize = mobileDesign.font_size ?? 24;
|
|
55
|
+
const textTransform = (mobileDesign.text_transform || design.text_transform || "uppercase") as React.CSSProperties["textTransform"];
|
|
56
|
+
const itemsGap = mobileDesign.items_gap ?? 32;
|
|
57
|
+
const itemsAlign = mobileDesign.items_align || "center";
|
|
58
|
+
const alignItems =
|
|
59
|
+
itemsAlign === "right" ? "flex-end" : itemsAlign === "left" ? "flex-start" : "center";
|
|
60
|
+
|
|
61
|
+
const fontFamily =
|
|
62
|
+
design.font_family || `var(--font-family, '${_cfg.typography.defaultFont}', ${_cfg.typography.monoFallback})`;
|
|
63
|
+
|
|
64
|
+
const logoLabel =
|
|
65
|
+
items.find((i) => i.type === "logo")?.label ||
|
|
66
|
+
design.logo_text ||
|
|
67
|
+
_cfg.defaults.logoText;
|
|
68
|
+
const menuItems = items
|
|
69
|
+
.filter((i) => i.type !== "logo" && i.visible !== false)
|
|
70
|
+
.sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0));
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
background: "#020202",
|
|
76
|
+
height: "100%",
|
|
77
|
+
display: "flex",
|
|
78
|
+
flexDirection: "column",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{/* Phone frame — centered */}
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
flex: 1,
|
|
85
|
+
display: "flex",
|
|
86
|
+
alignItems: "center",
|
|
87
|
+
justifyContent: "center",
|
|
88
|
+
padding: "20px 24px 12px",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
width: "100%",
|
|
94
|
+
maxWidth: 280,
|
|
95
|
+
borderRadius: 20,
|
|
96
|
+
overflow: "hidden",
|
|
97
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
98
|
+
background: overlayBg,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{/* Navbar bar — logo + hamburger icon */}
|
|
102
|
+
<div
|
|
103
|
+
style={{
|
|
104
|
+
display: "flex",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
justifyContent: "space-between",
|
|
107
|
+
paddingLeft: `${Math.min(barPaddingH, 24)}px`,
|
|
108
|
+
paddingRight: `${Math.min(barPaddingH, 24)}px`,
|
|
109
|
+
paddingTop: `${Math.min(barPaddingV, 20)}px`,
|
|
110
|
+
paddingBottom: `${Math.min(barPaddingV, 20)}px`,
|
|
111
|
+
background: barBgColor,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{/* Logo label */}
|
|
115
|
+
<span
|
|
116
|
+
style={{
|
|
117
|
+
color: hamburgerColor,
|
|
118
|
+
fontSize: Math.min(design.font_size ?? 14, 14),
|
|
119
|
+
fontWeight: design.font_weight || "400",
|
|
120
|
+
fontFamily,
|
|
121
|
+
textTransform,
|
|
122
|
+
letterSpacing: "0.05em",
|
|
123
|
+
overflow: "hidden",
|
|
124
|
+
textOverflow: "ellipsis",
|
|
125
|
+
whiteSpace: "nowrap",
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{logoLabel}
|
|
129
|
+
</span>
|
|
130
|
+
|
|
131
|
+
{/* Hamburger icon */}
|
|
132
|
+
<div
|
|
133
|
+
style={{
|
|
134
|
+
display: "flex",
|
|
135
|
+
flexDirection: "column",
|
|
136
|
+
gap: 3.5,
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<span
|
|
141
|
+
style={{
|
|
142
|
+
display: "block",
|
|
143
|
+
width: 16,
|
|
144
|
+
height: 1.5,
|
|
145
|
+
borderRadius: 1,
|
|
146
|
+
background: hamburgerColor,
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
<span
|
|
150
|
+
style={{
|
|
151
|
+
display: "block",
|
|
152
|
+
width: 16,
|
|
153
|
+
height: 1.5,
|
|
154
|
+
borderRadius: 1,
|
|
155
|
+
background: hamburgerColor,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
<span
|
|
159
|
+
style={{
|
|
160
|
+
display: "block",
|
|
161
|
+
width: 16,
|
|
162
|
+
height: 1.5,
|
|
163
|
+
borderRadius: 1,
|
|
164
|
+
background: hamburgerColor,
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Overlay — menu items */}
|
|
171
|
+
<div
|
|
172
|
+
style={{
|
|
173
|
+
display: "flex",
|
|
174
|
+
flexDirection: "column",
|
|
175
|
+
alignItems,
|
|
176
|
+
justifyContent: "center",
|
|
177
|
+
gap: `${Math.min(itemsGap, 40)}px`,
|
|
178
|
+
paddingLeft: `${Math.min(barPaddingH, 24)}px`,
|
|
179
|
+
paddingRight: `${Math.min(barPaddingH, 24)}px`,
|
|
180
|
+
paddingTop: 24,
|
|
181
|
+
paddingBottom: 32,
|
|
182
|
+
minHeight: 180,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{menuItems.map((item) => (
|
|
186
|
+
<span
|
|
187
|
+
key={item._key}
|
|
188
|
+
style={{
|
|
189
|
+
color: textColor,
|
|
190
|
+
fontSize: Math.min(fontSize, 28),
|
|
191
|
+
fontWeight: design.font_weight || "400",
|
|
192
|
+
fontFamily,
|
|
193
|
+
textTransform,
|
|
194
|
+
letterSpacing: "0.05em",
|
|
195
|
+
whiteSpace: "nowrap",
|
|
196
|
+
overflow: "hidden",
|
|
197
|
+
textOverflow: "ellipsis",
|
|
198
|
+
maxWidth: "100%",
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{item.label || "Untitled"}
|
|
202
|
+
</span>
|
|
203
|
+
))}
|
|
204
|
+
{menuItems.length === 0 && (
|
|
205
|
+
<span
|
|
206
|
+
style={{
|
|
207
|
+
color: "rgba(255,255,255,0.2)",
|
|
208
|
+
fontSize: 12,
|
|
209
|
+
fontStyle: "italic",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
No menu items
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Footer — matches NavLivePreview style */}
|
|
220
|
+
<div className="flex items-center justify-between px-3 py-1.5 text-[9px] text-neutral-600">
|
|
221
|
+
<span>Mobile Preview</span>
|
|
222
|
+
<span>{menuItems.length} menu items</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|