@morphika/andami 0.1.3 → 0.1.6

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 (105) hide show
  1. package/app/(site)/[slug]/page.tsx +7 -4
  2. package/app/(site)/layout.tsx +5 -2
  3. package/app/(site)/page.tsx +2 -2
  4. package/app/(site)/preview/page.tsx +4 -4
  5. package/app/(site)/work/[slug]/page.tsx +7 -4
  6. package/app/admin/layout.tsx +3 -2
  7. package/app/admin/login/page.tsx +5 -5
  8. package/app/admin/navigation/page.tsx +255 -157
  9. package/app/api/admin/assets/health/route.ts +1 -1
  10. package/app/api/admin/assets/register/route.ts +1 -1
  11. package/app/api/admin/assets/registry/route.ts +1 -1
  12. package/app/api/admin/assets/relink/confirm/route.ts +2 -2
  13. package/app/api/admin/assets/relink/route.ts +1 -1
  14. package/app/api/admin/assets/scan/route.ts +1 -1
  15. package/app/api/admin/custom-sections/[slug]/route.ts +1 -1
  16. package/app/api/admin/custom-sections/route.ts +1 -1
  17. package/app/api/admin/database/route.ts +1 -1
  18. package/app/api/admin/pages/[slug]/duplicate/route.ts +1 -1
  19. package/app/api/admin/pages/[slug]/route.ts +2 -2
  20. package/app/api/admin/pages/[slug]/set-home/route.ts +1 -1
  21. package/app/api/admin/pages/route.ts +1 -1
  22. package/app/api/admin/preview/route.ts +1 -1
  23. package/app/api/admin/r2/delete/route.ts +1 -1
  24. package/app/api/admin/r2/rename/route.ts +1 -1
  25. package/app/api/admin/r2/status/route.ts +1 -1
  26. package/app/api/admin/r2/upload-url/route.ts +1 -1
  27. package/app/api/admin/settings/route.ts +41 -16
  28. package/app/api/admin/setup/complete/route.ts +2 -2
  29. package/app/api/admin/setup/route.ts +7 -4
  30. package/app/api/admin/storage/switch/route.ts +1 -1
  31. package/app/api/admin/styles/route.ts +1 -1
  32. package/components/admin/index.ts +7 -0
  33. package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
  34. package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
  35. package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
  36. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
  37. package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
  38. package/components/admin/nav-builder/index.ts +2 -0
  39. package/components/blocks/BlockRenderer.tsx +65 -13
  40. package/components/blocks/ButtonBlockRenderer.tsx +29 -6
  41. package/components/blocks/CoverBlockRenderer.tsx +36 -14
  42. package/components/blocks/ImageBlockRenderer.tsx +5 -3
  43. package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
  44. package/components/blocks/PageRenderer.tsx +4 -2
  45. package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
  46. package/components/blocks/SectionRenderer.tsx +9 -8
  47. package/components/blocks/SectionV2Renderer.tsx +8 -8
  48. package/components/blocks/SpacerBlockRenderer.tsx +4 -2
  49. package/components/blocks/TextBlockRenderer.tsx +9 -4
  50. package/components/builder/BuilderCanvas.tsx +10 -4
  51. package/components/builder/ColorPicker.tsx +51 -243
  52. package/components/builder/ColorSwatchPicker.tsx +214 -274
  53. package/components/builder/DndWrapper.tsx +5 -2
  54. package/components/builder/SectionV2Canvas.tsx +15 -4
  55. package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -0
  57. package/components/builder/color-picker/AngleControl.tsx +138 -0
  58. package/components/builder/color-picker/ColorInputs.tsx +105 -0
  59. package/components/builder/color-picker/EyedropperButton.tsx +74 -0
  60. package/components/builder/color-picker/GradientBar.tsx +222 -0
  61. package/components/builder/color-picker/GradientPreview.tsx +53 -0
  62. package/components/builder/color-picker/HueSlider.tsx +124 -0
  63. package/components/builder/color-picker/MeshCanvas.tsx +172 -0
  64. package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
  65. package/components/builder/color-picker/MeshPointList.tsx +200 -0
  66. package/components/builder/color-picker/PositionControl.tsx +158 -0
  67. package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
  68. package/components/builder/color-picker/StopEditor.tsx +178 -0
  69. package/components/builder/color-picker/SwatchBar.tsx +93 -0
  70. package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
  71. package/components/builder/color-picker/index.ts +62 -0
  72. package/components/builder/color-picker/types.ts +115 -0
  73. package/components/builder/color-picker/utils.ts +138 -0
  74. package/components/builder/editors/CoverBlockEditor.tsx +86 -32
  75. package/components/builder/editors/ProjectGridEditor.tsx +51 -4
  76. package/components/builder/hooks/useColumnDrag.ts +25 -27
  77. package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
  78. package/components/builder/settings-panel/LayoutTab.tsx +382 -310
  79. package/components/builder/settings-panel/PageSettings.tsx +6 -4
  80. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  81. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
  82. package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
  83. package/components/ui/Navbar.tsx +97 -25
  84. package/components/ui/PortfolioTracker.tsx +3 -3
  85. package/lib/assets.ts +1 -1
  86. package/lib/auth.ts +1 -1
  87. package/lib/builder/gradient-presets.ts +128 -0
  88. package/lib/builder/layout-styles.ts +16 -10
  89. package/lib/builder/serializer.ts +1 -0
  90. package/lib/builder/store-blocks.ts +48 -61
  91. package/lib/builder/store-helpers.ts +31 -14
  92. package/lib/builder/store.ts +59 -41
  93. package/lib/builder/types.ts +14 -0
  94. package/lib/color-utils.ts +200 -0
  95. package/lib/revalidate.ts +2 -2
  96. package/lib/sanity/client.ts +16 -0
  97. package/lib/sanity/queries.ts +4 -3
  98. package/lib/sanity/types.ts +76 -1
  99. package/lib/setup/detect.ts +1 -1
  100. package/lib/storage/index.ts +22 -4
  101. package/lib/version.ts +6 -0
  102. package/package.json +8 -2
  103. package/sanity/schemas/siteSettings.ts +34 -0
  104. package/styles/base.css +3 -3
  105. package/app/globals.css +0 -7
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
4
4
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
@@ -210,7 +210,7 @@ export async function POST(request: NextRequest) {
210
210
  // Also update siteSettings seed_url if it changed
211
211
  if (new_seed_url && new_seed_url !== oldSeed) {
212
212
  const settings = await client.fetch(
213
- `*[_type == "siteSettings"][0]._id`
213
+ `*[_id == "siteSettings"][0]._id`
214
214
  );
215
215
  if (settings) {
216
216
  transaction.patch(settings, {
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { getStorageAdapter } from "../../../../../lib/storage";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
5
  import { customSectionBySlugQuery, pagesUsingCustomSectionQuery } from "../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../lib/auth";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../lib/sanity/writeClient";
4
4
  import { allCustomSectionsQuery } from "../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../lib/auth";
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
 
5
5
  /**
6
6
  * GET /api/admin/database — Sanity connection status & stats
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
5
5
  import { pageBySlugQuery } from "../../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
5
  import { pageBySlugQuery } from "../../../../../lib/sanity/queries";
6
6
  import { isAdminAuthenticated } from "../../../../../lib/auth";
@@ -577,7 +577,7 @@ export async function DELETE(
577
577
  if (referencing > 0) {
578
578
  // Remove navigation references to this page before deleting
579
579
  const siteSettings = await client.fetch(
580
- `*[_type == "siteSettings"][0]{ _id, nav_items }`
580
+ `*[_id == "siteSettings"][0]{ _id, nav_items }`
581
581
  );
582
582
  if (siteSettings?.nav_items?.length) {
583
583
  const filtered = siteSettings.nav_items.filter((item: RawNavItem) =>
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
- import { client } from "../../../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../../../lib/sanity/writeClient";
5
5
  import { isAdminAuthenticated } from "../../../../../../lib/auth";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../lib/sanity/writeClient";
4
4
  import { allPagesQuery } from "../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../lib/auth";
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { client } from "../../../../lib/sanity/client";
2
+ import { adminClient as client } from "../../../../lib/sanity/client";
3
3
  import { pageBySlugQuery } from "../../../../lib/sanity/queries";
4
4
  import { isAdminAuthenticated } from "../../../../lib/auth";
5
5
  import { logger } from "../../../../lib/logger";
@@ -3,7 +3,7 @@ import { S3Client, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsComma
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
- import { client } from "../../../../../lib/sanity/client";
6
+ import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { auditLog } from "../../../../../lib/audit";
@@ -3,7 +3,7 @@ import { S3Client, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command,
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
- import { client } from "../../../../../lib/sanity/client";
6
+ import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { auditLog } from "../../../../../lib/audit";
@@ -1,7 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
- import { client } from "../../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../../lib/sanity/client";
5
5
  import { decryptToken } from "../../../../../lib/security";
6
6
  import { logger } from "../../../../../lib/logger";
7
7
 
@@ -4,7 +4,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
6
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
7
- import { client } from "../../../../../lib/sanity/client";
7
+ import { adminClient as client } from "../../../../../lib/sanity/client";
8
8
  import { decryptToken } from "../../../../../lib/security";
9
9
  import { getMimeType, isMediaFile } from "../../../../../lib/storage/types";
10
10
  import { logger } from "../../../../../lib/logger";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../lib/sanity/writeClient";
5
5
  import { siteSettingsQuery } from "../../../../lib/sanity/queries";
6
6
  import { isSafeUrl, isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../lib/security";
@@ -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: cfg.defaults.metaTitle,
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
- if (!["internal", "external", "content"].includes(item.link_type)) {
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: "link_type must be 'internal', 'external', or 'content'" },
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 (item.link_type === "external" && item.external_url) {
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 (item.link_type === "content" && item.content_type === "video-embed" && item.content_url) {
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: string;
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) : cfg.defaults.logoText,
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, 50) : "",
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) {
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../../lib/auth";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
- import { client } from "../../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../../lib/sanity/client";
5
5
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
6
  import { auditLog } from "../../../../../lib/audit";
7
7
  import { logger } from "../../../../../lib/logger";
@@ -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
- `*[_type == "siteSettings"][0]{ _id }`
29
+ `*[_id == "siteSettings"][0]{ _id }`
30
30
  );
31
31
 
32
32
  if (!doc) {
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
3
  import { getSetupStatus } from "../../../../lib/setup/detect";
4
- import { client } from "../../../../lib/sanity/client";
4
+ import { adminClient as client } from "../../../../lib/sanity/client";
5
5
  import { writeClient } from "../../../../lib/sanity/writeClient";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
7
7
 
@@ -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.create({
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.create({
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.create({
105
+ await writeClient.createIfNotExists({
106
+ _id: "assetRegistry",
104
107
  _type: "assetRegistry",
105
108
  assets: [],
106
109
  storage_provider: "",
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
- import { client } from "../../../../../lib/sanity/client";
5
+ import { adminClient as client } from "../../../../../lib/sanity/client";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
7
7
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
8
8
  import { invalidateProviderConfigCache } from "../../../../../lib/storage";
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../lib/auth";
3
- import { client } from "../../../../lib/sanity/client";
3
+ import { adminClient as client } from "../../../../lib/sanity/client";
4
4
  import { writeClient } from "../../../../lib/sanity/writeClient";
5
5
  import { siteStylesQuery } from "../../../../lib/sanity/queries";
6
6
  import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
@@ -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
- <SelectInput
126
- value={design.color || "yellow-lime"}
127
- onChange={(v) => update({ color: v as NavDesign["color"] })}
128
- options={[
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
- <ColorInput
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 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">
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
- <ColorInput
290
+ <ColorSwatchPicker
267
291
  value={item.style_overrides?.color || ""}
268
- onChange={(v) => updateOverride("color", v || undefined)}
269
- placeholder="Inherit"
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 color = NAV_COLOR_HEX[design.color || "yellow-lime"];
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;