@morphika/andami 0.5.1 → 0.5.3

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 (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. package/styles/base.css +114 -114
@@ -3,7 +3,7 @@ import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { writeClient } from "../../../../../lib/sanity/writeClient";
4
4
  import { assetRegistryQuery } from "../../../../../lib/sanity/queries";
5
5
  import { isAdminAuthenticated } from "../../../../../lib/auth";
6
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
7
7
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../../lib/security";
8
8
  import { logger } from "../../../../../lib/logger";
9
9
 
@@ -50,6 +50,9 @@ export async function POST(request: NextRequest) {
50
50
  if (!validateCsrf(request)) {
51
51
  return csrfErrorResponse();
52
52
  }
53
+ if (!hasJsonContentType(request)) {
54
+ return contentTypeErrorResponse();
55
+ }
53
56
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
54
57
  return NextResponse.json({ error: "Request body too large" }, { status: 413 });
55
58
  }
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
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
- import { validateCsrf, csrfErrorResponse } from "../../../../../../lib/csrf";
5
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../../lib/csrf";
6
6
  import { logger } from "../../../../../../lib/logger";
7
7
 
8
8
  /**
@@ -74,6 +74,9 @@ export async function POST(request: NextRequest) {
74
74
  if (!validateCsrf(request)) {
75
75
  return csrfErrorResponse();
76
76
  }
77
+ if (!hasJsonContentType(request)) {
78
+ return contentTypeErrorResponse();
79
+ }
77
80
 
78
81
  try {
79
82
  const body = await request.json();
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { adminClient as client } from "../../../../../lib/sanity/client";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
4
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
5
5
  import { getStorageAdapter } from "../../../../../lib/storage";
6
6
  import { logger } from "../../../../../lib/logger";
7
7
  import type { RegisteredAsset } from "../../../../../lib/sanity/types";
@@ -39,6 +39,9 @@ export async function POST(request: NextRequest) {
39
39
  if (!validateCsrf(request)) {
40
40
  return csrfErrorResponse();
41
41
  }
42
+ if (!hasJsonContentType(request)) {
43
+ return contentTypeErrorResponse();
44
+ }
42
45
 
43
46
  try {
44
47
  const body = await request.json();
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
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
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
6
6
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../../lib/security";
7
7
  import { getStorageAdapter, getActiveProvider } from "../../../../../lib/storage";
8
8
  import { logger } from "../../../../../lib/logger";
@@ -29,6 +29,9 @@ export async function POST(request: NextRequest) {
29
29
  if (!validateCsrf(request)) {
30
30
  return csrfErrorResponse();
31
31
  }
32
+ if (!hasJsonContentType(request)) {
33
+ return contentTypeErrorResponse();
34
+ }
32
35
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
33
36
  return NextResponse.json({ error: "Request body too large" }, { status: 413 });
34
37
  }
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { isAdminAuthenticated } from "../../../../../lib/auth";
3
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
3
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
4
4
  import {
5
5
  checkRateLimit,
6
6
  jsonError,
@@ -92,6 +92,9 @@ export async function POST(request: NextRequest) {
92
92
  if (!validateCsrf(request)) {
93
93
  return csrfErrorResponse();
94
94
  }
95
+ if (!hasJsonContentType(request)) {
96
+ return contentTypeErrorResponse();
97
+ }
95
98
 
96
99
  // M-2: reject oversize bodies up front so we don't spend CPU on
97
100
  // `request.json()` for a payload we'll refuse anyway. Vercel's platform
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { S3Client, HeadBucketCommand, PutBucketCorsCommand } from "@aws-sdk/client-s3";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
6
6
  import { encryptToken, isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
7
7
  import { logger } from "../../../../../lib/logger";
8
8
  import { invalidateProviderConfigCache } from "../../../../../lib/storage";
@@ -25,6 +25,9 @@ export async function POST(request: NextRequest) {
25
25
  if (!validateCsrf(request)) {
26
26
  return csrfErrorResponse();
27
27
  }
28
+ if (!hasJsonContentType(request)) {
29
+ return contentTypeErrorResponse();
30
+ }
28
31
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
29
32
  return jsonError("Request body too large", 413);
30
33
  }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { S3Client, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from "@aws-sdk/client-s3";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
4
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
6
  import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
@@ -27,6 +27,9 @@ export async function POST(request: NextRequest) {
27
27
  if (!validateCsrf(request)) {
28
28
  return csrfErrorResponse();
29
29
  }
30
+ if (!hasJsonContentType(request)) {
31
+ return contentTypeErrorResponse();
32
+ }
30
33
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
31
34
  return jsonError("Request body too large", 413);
32
35
  }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { S3Client, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } from "@aws-sdk/client-s3";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
4
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
5
5
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, checkRateLimit } from "../../../../../lib/security";
6
6
  import { adminClient as client } from "../../../../../lib/sanity/client";
7
7
  import { writeClient } from "../../../../../lib/sanity/writeClient";
@@ -29,6 +29,9 @@ export async function POST(request: NextRequest) {
29
29
  if (!validateCsrf(request)) {
30
30
  return csrfErrorResponse();
31
31
  }
32
+ if (!hasJsonContentType(request)) {
33
+ return contentTypeErrorResponse();
34
+ }
32
35
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
33
36
  return jsonError("Request body too large", 413);
34
37
  }
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { isAdminAuthenticated } from "../../../../../lib/auth";
5
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
5
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
6
6
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, isValidUploadKey, checkRateLimit } from "../../../../../lib/security";
7
7
  import { adminClient as client } from "../../../../../lib/sanity/client";
8
8
  import { decryptToken } from "../../../../../lib/security";
@@ -27,6 +27,9 @@ export async function POST(request: NextRequest) {
27
27
  if (!validateCsrf(request)) {
28
28
  return csrfErrorResponse();
29
29
  }
30
+ if (!hasJsonContentType(request)) {
31
+ return contentTypeErrorResponse();
32
+ }
30
33
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
31
34
  return jsonError("Request body too large", 413);
32
35
  }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { revalidatePath } from "next/cache";
3
3
  import { isAdminAuthenticated } from "../../../../lib/auth";
4
- import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
4
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../lib/csrf";
5
5
  import { logger } from "../../../../lib/logger";
6
6
 
7
7
  /**
@@ -23,6 +23,9 @@ export async function POST(request: NextRequest) {
23
23
  if (!validateCsrf(request)) {
24
24
  return csrfErrorResponse();
25
25
  }
26
+ if (!hasJsonContentType(request)) {
27
+ return contentTypeErrorResponse();
28
+ }
26
29
 
27
30
  try {
28
31
  const body = await request.json().catch(() => ({}));
@@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache";
3
3
  import { isAdminAuthenticated } from "../../../../../lib/auth";
4
4
  import { writeClient } from "../../../../../lib/sanity/writeClient";
5
5
  import { adminClient as client } from "../../../../../lib/sanity/client";
6
- import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
6
+ import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
7
7
  import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
8
8
  import { invalidateProviderConfigCache } from "../../../../../lib/storage";
9
9
  import { auditLog } from "../../../../../lib/audit";
@@ -36,6 +36,9 @@ export async function POST(request: NextRequest) {
36
36
  if (!validateCsrf(request)) {
37
37
  return csrfErrorResponse();
38
38
  }
39
+ if (!hasJsonContentType(request)) {
40
+ return contentTypeErrorResponse();
41
+ }
39
42
  if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
40
43
  return jsonError("Request body too large", 413);
41
44
  }
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { adminClient } from "../../../../lib/sanity/client";
2
+ import { client } from "../../../../lib/sanity/client";
3
3
  import { customSectionByIdQuery } from "../../../../lib/sanity/queries";
4
4
  import { logger } from "../../../../lib/logger";
5
5
 
@@ -11,10 +11,9 @@ type RouteContext = { params: Promise<{ id: string }> };
11
11
  * Returns only the `section` field (PageSectionV2 data) for rendering
12
12
  * custom section instances on the public site.
13
13
  *
14
- * Uses adminClient (useCdn: false) so data is always fresh after
15
- * revalidatePath() is called from the admin PATCH endpoint.
16
- * Cache is controlled via ISR revalidation on-demand, not via
17
- * Cache-Control headers — this avoids stale CDN responses after edits.
14
+ * Uses the public `client` (useCdn: true) to hit Sanity's CDN (~2ms) instead
15
+ * of the API (~200ms). Freshness is handled by `revalidatePath()` in the
16
+ * admin PATCH endpoint, which purges this endpoint's ISR cache on save.
18
17
  */
19
18
  export async function GET(_request: NextRequest, context: RouteContext) {
20
19
  try {
@@ -24,7 +23,7 @@ export async function GET(_request: NextRequest, context: RouteContext) {
24
23
  return NextResponse.json({ error: "Missing section ID" }, { status: 400 });
25
24
  }
26
25
 
27
- const result = await adminClient.fetch(customSectionByIdQuery, { id });
26
+ const result = await client.fetch(customSectionByIdQuery, { id });
28
27
  if (!result?.section) {
29
28
  return NextResponse.json({ error: "Custom section not found" }, { status: 404 });
30
29
  }
@@ -71,7 +71,7 @@ export default function MetadataEditor({
71
71
  value={title}
72
72
  onChange={(e) => setTitle(e.target.value)}
73
73
  placeholder={getSiteConfig().defaults.metaTitle}
74
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
74
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
75
75
  />
76
76
  <p className="text-xs text-neutral-400">
77
77
  Shown in browser tabs and search results when no page-specific title is
@@ -89,7 +89,7 @@ export default function MetadataEditor({
89
89
  onChange={(e) => setDescription(e.target.value)}
90
90
  placeholder="Motion graphics studio based in Barcelona..."
91
91
  rows={3}
92
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none resize-none"
92
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none resize-none"
93
93
  />
94
94
  <div className="flex justify-between">
95
95
  <p className="text-xs text-neutral-400">
@@ -115,7 +115,7 @@ export default function MetadataEditor({
115
115
  value={ogImage}
116
116
  onChange={(e) => setOgImage(e.target.value)}
117
117
  placeholder="meta/og-image.jpg"
118
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
118
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
119
119
  />
120
120
  <p className="text-xs text-neutral-400">
121
121
  Relative path to the default image shown when sharing on social media.
@@ -133,7 +133,7 @@ export default function MetadataEditor({
133
133
  value={favicon}
134
134
  onChange={(e) => setFavicon(e.target.value)}
135
135
  placeholder="meta/favicon.ico"
136
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
136
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
137
137
  />
138
138
  <p className="text-xs text-neutral-400">
139
139
  Relative path to the favicon. Resolved via the asset seed URL.
@@ -150,7 +150,7 @@ export default function MetadataEditor({
150
150
  value={analyticsId}
151
151
  onChange={(e) => setAnalyticsId(e.target.value)}
152
152
  placeholder="G-XXXXXXXXXX or plausible domain"
153
- className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#076bff] focus:outline-none"
153
+ className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2.5 text-sm text-neutral-900 focus:border-[#3580f9] focus:outline-none"
154
154
  />
155
155
  <p className="text-xs text-neutral-400">
156
156
  Google Analytics measurement ID or Plausible domain. Leave empty to
@@ -163,7 +163,7 @@ export default function MetadataEditor({
163
163
  <button
164
164
  onClick={handleSave}
165
165
  disabled={saving}
166
- className="rounded-lg bg-[#076bff] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#0559d4] transition-colors disabled:opacity-50"
166
+ className="rounded-lg bg-[#3580f9] px-5 py-2.5 text-sm font-medium text-white hover:bg-[#2d6dd4] transition-colors disabled:opacity-50"
167
167
  >
168
168
  {saving ? "Saving..." : "Save Metadata"}
169
169
  </button>
@@ -96,7 +96,7 @@ export default function PublishToggle(props: PublishToggleProps) {
96
96
  cursor: "pointer",
97
97
  whiteSpace: "nowrap",
98
98
  transition: "background-color 150ms, color 150ms",
99
- backgroundColor: isDraft ? "#f59e0b" : "transparent",
99
+ backgroundColor: isDraft ? "#d97706" : "transparent",
100
100
  color: isDraft ? "#fff" : "#a3a3a3",
101
101
  ...(busy ? { opacity: 0.5 } : {}),
102
102
  }}
@@ -118,7 +118,7 @@ export default function PublishToggle(props: PublishToggleProps) {
118
118
  cursor: "pointer",
119
119
  whiteSpace: "nowrap",
120
120
  transition: "background-color 150ms, color 150ms",
121
- backgroundColor: !isDraft ? "#10b981" : "transparent",
121
+ backgroundColor: !isDraft ? "#059669" : "transparent",
122
122
  color: !isDraft ? "#fff" : "#a3a3a3",
123
123
  ...(busy ? { opacity: 0.5 } : {}),
124
124
  }}
@@ -220,7 +220,7 @@ export default function NavBuilder({
220
220
  className={`px-4 py-1.5 text-[11px] font-medium rounded-lg transition-all ${
221
221
  saving || !hasChanges
222
222
  ? "bg-white/40 text-neutral-400 cursor-not-allowed"
223
- : "bg-white text-[#076bff] shadow-sm hover:shadow-md"
223
+ : "bg-white text-[#3580f9] shadow-sm hover:shadow-md"
224
224
  }`}
225
225
  >
226
226
  {saving ? "Saving..." : "Save"}
@@ -87,7 +87,7 @@ function DroppableCell({
87
87
  <div ref={setNodeRef} style={{ gridColumn: column, gridRow: 1 }}>
88
88
  <NavGridCell column={column} onAddItem={onAddItem} />
89
89
  {isDragOver && (
90
- <div className="absolute inset-0 rounded-lg ring-2 ring-[#076bff] bg-[#076bff]/10 pointer-events-none" />
90
+ <div className="absolute inset-0 rounded-lg ring-2 ring-[#3580f9] bg-[#3580f9]/10 pointer-events-none" />
91
91
  )}
92
92
  </div>
93
93
  );
@@ -286,7 +286,7 @@ export default function NavBuilderGrid({
286
286
  {/* Helper hints */}
287
287
  <div className="mt-4 flex gap-4 text-[10px] text-neutral-400">
288
288
  <span>
289
- <span className="text-[#076bff]">+</span> Click empty column to add
289
+ <span className="text-[#3580f9]">+</span> Click empty column to add
290
290
  </span>
291
291
  <span>
292
292
  <span className="text-neutral-500">&#x2807;</span> Drag to move
@@ -301,7 +301,7 @@ export default function NavBuilderGrid({
301
301
  <DragOverlay>
302
302
  {activeItem ? (
303
303
  <div
304
- className="h-14 flex items-center px-3 gap-2 rounded-lg ring-2 ring-[#076bff] bg-white shadow-xl"
304
+ className="h-14 flex items-center px-3 gap-2 rounded-lg ring-2 ring-[#3580f9] bg-white shadow-xl"
305
305
  style={{ width: 120, opacity: 0.9 }}
306
306
  >
307
307
  <div className="text-xs font-mono text-neutral-900 uppercase tracking-wider truncate">
@@ -1,48 +1,48 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
-
5
- interface NavGridCellProps {
6
- column: number;
7
- onAddItem: (column: number) => void;
8
- }
9
-
10
- export default function NavGridCell({ column, onAddItem }: NavGridCellProps) {
11
- const [hovered, setHovered] = useState(false);
12
-
13
- return (
14
- <div
15
- onMouseEnter={() => setHovered(true)}
16
- onMouseLeave={() => setHovered(false)}
17
- onClick={(e) => {
18
- e.stopPropagation();
19
- onAddItem(column);
20
- }}
21
- className={`h-14 flex items-center justify-center cursor-pointer transition-all rounded-lg border border-dashed ${
22
- hovered
23
- ? "border-[#076bff]/50 bg-[#076bff]/[0.03]"
24
- : "border-neutral-200 bg-transparent"
25
- }`}
26
- data-nav-cell={column}
27
- >
28
- <div
29
- className={`flex flex-col items-center gap-0.5 transition-all ${
30
- hovered ? "text-[#076bff]" : "text-neutral-300"
31
- }`}
32
- style={{
33
- opacity: hovered ? 1 : 0.5,
34
- transform: hovered ? "scale(1)" : "scale(0.9)",
35
- }}
36
- >
37
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
38
- <path
39
- d="M8 3v10M3 8h10"
40
- stroke="currentColor"
41
- strokeWidth="1.5"
42
- strokeLinecap="round"
43
- />
44
- </svg>
45
- </div>
46
- </div>
47
- );
48
- }
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ interface NavGridCellProps {
6
+ column: number;
7
+ onAddItem: (column: number) => void;
8
+ }
9
+
10
+ export default function NavGridCell({ column, onAddItem }: NavGridCellProps) {
11
+ const [hovered, setHovered] = useState(false);
12
+
13
+ return (
14
+ <div
15
+ onMouseEnter={() => setHovered(true)}
16
+ onMouseLeave={() => setHovered(false)}
17
+ onClick={(e) => {
18
+ e.stopPropagation();
19
+ onAddItem(column);
20
+ }}
21
+ className={`h-14 flex items-center justify-center cursor-pointer transition-all rounded-lg border border-dashed ${
22
+ hovered
23
+ ? "border-[#3580f9]/50 bg-[#3580f9]/[0.03]"
24
+ : "border-neutral-200 bg-transparent"
25
+ }`}
26
+ data-nav-cell={column}
27
+ >
28
+ <div
29
+ className={`flex flex-col items-center gap-0.5 transition-all ${
30
+ hovered ? "text-[#3580f9]" : "text-neutral-300"
31
+ }`}
32
+ style={{
33
+ opacity: hovered ? 1 : 0.5,
34
+ transform: hovered ? "scale(1)" : "scale(0.9)",
35
+ }}
36
+ >
37
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
38
+ <path
39
+ d="M8 3v10M3 8h10"
40
+ stroke="currentColor"
41
+ strokeWidth="1.5"
42
+ strokeLinecap="round"
43
+ />
44
+ </svg>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
@@ -3,6 +3,7 @@
3
3
  import { useState, useRef, useCallback, useEffect } from "react";
4
4
  import type { NavItem } from "../../../lib/sanity/types";
5
5
  import { TOTAL_COLUMNS, getMaxSpan } from "./nav-builder-utils";
6
+ import { BubbleTooltip } from "../../builder/BubbleIcons";
6
7
 
7
8
  interface NavGridItemProps {
8
9
  item: NavItem;
@@ -94,7 +95,7 @@ export default function NavGridItem({
94
95
  }}
95
96
  className={`h-14 relative flex items-center px-3 gap-2 cursor-pointer rounded-lg transition-all border ${
96
97
  isSelected
97
- ? "border-[#076bff] bg-[#076bff]/5 shadow-sm"
98
+ ? "border-[#3580f9] bg-[#3580f9]/5 shadow-sm"
98
99
  : hovered
99
100
  ? "border-neutral-300 bg-neutral-50"
100
101
  : "border-neutral-200 bg-white"
@@ -141,7 +142,7 @@ export default function NavGridItem({
141
142
  <div
142
143
  className="absolute -top-2 -right-2 text-[9px] font-semibold px-1.5 py-0 rounded text-white"
143
144
  style={{
144
- background: isSelected ? "#076bff" : "#a3a3a3",
145
+ background: isSelected ? "#3580f9" : "#a3a3a3",
145
146
  }}
146
147
  >
147
148
  {item.column_span}/{TOTAL_COLUMNS}
@@ -155,8 +156,8 @@ export default function NavGridItem({
155
156
  e.stopPropagation();
156
157
  onDelete(item._key);
157
158
  }}
158
- className="absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
159
- title="Remove item"
159
+ className="group/bb absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
160
+ aria-label="Remove item"
160
161
  >
161
162
  <svg width="10" height="10" viewBox="0 0 10 10">
162
163
  <path
@@ -166,6 +167,7 @@ export default function NavGridItem({
166
167
  strokeLinecap="round"
167
168
  />
168
169
  </svg>
170
+ <BubbleTooltip>Remove item</BubbleTooltip>
169
171
  </button>
170
172
  )}
171
173
 
@@ -175,9 +177,9 @@ export default function NavGridItem({
175
177
  className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-6 rounded cursor-col-resize transition-all"
176
178
  style={{
177
179
  background: resizing
178
- ? "#076bff"
180
+ ? "#3580f9"
179
181
  : isSelected
180
- ? "#076bff"
182
+ ? "#3580f9"
181
183
  : hovered
182
184
  ? "#a3a3a3"
183
185
  : "transparent",