@morphika/webframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +210 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,304 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { isAdminAuthenticated } from "../../../../lib/auth";
3
+ import { client } from "../../../../lib/sanity/client";
4
+ import { writeClient } from "../../../../lib/sanity/writeClient";
5
+ import { siteStylesQuery } from "../../../../lib/sanity/queries";
6
+ import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
7
+ import { isBodyTooLarge, MAX_JSON_BODY_SIZE } from "../../../../lib/security";
8
+ import { logger } from "../../../../lib/logger";
9
+ import { DEFAULT_GRID_WIDTH } from "../../../../lib/builder/constants";
10
+ import { getSiteConfig } from "../../../../lib/config";
11
+
12
+ /**
13
+ * GET /api/admin/styles — fetch siteStyles document
14
+ * POST /api/admin/styles — update siteStyles sections (authenticated)
15
+ */
16
+
17
+ const STYLES_ID = "siteStyles";
18
+
19
+ // Default values for a fresh siteStyles document.
20
+ // Reads font name and palette from site.config.ts so different instances
21
+ // get their own defaults without changing this file.
22
+ function getDefaultStyles() {
23
+ const cfg = getSiteConfig();
24
+ const font = cfg.typography.defaultFont;
25
+ const pal = cfg.palette;
26
+
27
+ // Build built-in font entries from config
28
+ const builtinFonts = cfg.typography.builtinFonts || [];
29
+ const fontEntries = builtinFonts.length > 0
30
+ ? [{
31
+ _key: "builtin_default",
32
+ family: font,
33
+ is_builtin: true,
34
+ variants: builtinFonts
35
+ .filter((f) => f.family === font)
36
+ .map((f, i) => ({
37
+ _key: `builtin_${i}`,
38
+ weight: f.weight || "400",
39
+ style: (f.style || "normal") as "normal" | "italic",
40
+ file_url: f.src,
41
+ original_filename: f.src.split("/").pop() || "",
42
+ })),
43
+ }]
44
+ : [];
45
+
46
+ return {
47
+ _id: STYLES_ID,
48
+ _type: "siteStyles",
49
+ fonts: fontEntries,
50
+ typography_h1: { font_family: font, font_size: "3rem", font_weight: "700", line_height: "1.1", letter_spacing: "-0.02em", text_transform: "none" },
51
+ typography_h2: { font_family: font, font_size: "2rem", font_weight: "700", line_height: "1.2", letter_spacing: "-0.01em", text_transform: "none" },
52
+ typography_h3: { font_family: font, font_size: "1.5rem", font_weight: "500", line_height: "1.3", letter_spacing: "0", text_transform: "none" },
53
+ typography_h4: { font_family: font, font_size: "1.125rem", font_weight: "500", line_height: "1.4", letter_spacing: "0", text_transform: "none" },
54
+ typography_body: { font_family: font, font_size: "0.875rem", font_weight: "400", line_height: "1.6", letter_spacing: "0", text_transform: "none" },
55
+ typography_small: { font_family: font, font_size: "0.75rem", font_weight: "400", line_height: "1.5", letter_spacing: "0.02em", text_transform: "none" },
56
+ grid_width: DEFAULT_GRID_WIDTH,
57
+ grid_outer_padding: "30",
58
+ grid_gutter_desktop: "30",
59
+ grid_gutter_responsive: "30",
60
+ grid_gutter_phone: "16",
61
+ color_palette: [],
62
+ // Legacy fields — use config palette as defaults
63
+ color_background: pal.background,
64
+ color_text: pal.text,
65
+ color_primary: pal.primary,
66
+ color_secondary: pal.secondary,
67
+ color_accent: pal.accentAlt || pal.accent,
68
+ color_muted: pal.muted,
69
+ link_color: pal.primary,
70
+ link_hover_color: pal.primary,
71
+ link_underline: true,
72
+ button_primary_bg: "#ffffff",
73
+ button_primary_text: "#000000",
74
+ button_secondary_bg: "#1a1a1a",
75
+ button_secondary_text: "#ffffff",
76
+ button_border_radius: "8px",
77
+ };
78
+ }
79
+
80
+ export async function GET() {
81
+ const authenticated = await isAdminAuthenticated();
82
+ if (!authenticated) {
83
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
84
+ }
85
+
86
+ try {
87
+ const cfg = getSiteConfig();
88
+ let styles = await client.fetch(siteStylesQuery);
89
+
90
+ // Auto-create if missing
91
+ if (!styles) {
92
+ await writeClient.createIfNotExists(getDefaultStyles());
93
+ styles = await client.fetch(siteStylesQuery);
94
+ }
95
+
96
+ // Transform flat Sanity fields → nested SiteStyles structure
97
+ const transformed = {
98
+ grid: {
99
+ width: styles.grid_width || DEFAULT_GRID_WIDTH,
100
+ outer_padding: styles.grid_outer_padding || "30",
101
+ gutter_desktop: styles.grid_gutter_desktop || "30",
102
+ gutter_responsive: styles.grid_gutter_responsive || "30",
103
+ gutter_phone: styles.grid_gutter_phone || "16",
104
+ },
105
+ fonts: styles.fonts || [],
106
+ typography: {
107
+ h1: styles.typography_h1 || null,
108
+ h2: styles.typography_h2 || null,
109
+ h3: styles.typography_h3 || null,
110
+ h4: styles.typography_h4 || null,
111
+ body: styles.typography_body || null,
112
+ small: styles.typography_small || null,
113
+ },
114
+ colors: {
115
+ swatches: (styles.color_palette || []).map((s: { _key?: string; name?: string; hex?: string }) => ({
116
+ _key: s._key || "",
117
+ name: s.name || "",
118
+ hex: s.hex || "#000000",
119
+ })),
120
+ // Legacy fields — config palette as fallback
121
+ background: styles.color_background || cfg.palette.background,
122
+ text: styles.color_text || cfg.palette.text,
123
+ primary: styles.color_primary || cfg.palette.primary,
124
+ secondary: styles.color_secondary || cfg.palette.secondary,
125
+ accent: styles.color_accent || cfg.palette.accent,
126
+ muted: styles.color_muted || cfg.palette.muted,
127
+ },
128
+ link_style: {
129
+ color: styles.link_color || cfg.palette.primary,
130
+ hover_color: styles.link_hover_color || cfg.palette.primary,
131
+ underline: styles.link_underline ?? true,
132
+ },
133
+ button_style: {
134
+ primary_bg: styles.button_primary_bg || "#ffffff",
135
+ primary_text: styles.button_primary_text || "#000000",
136
+ secondary_bg: styles.button_secondary_bg || "#1a1a1a",
137
+ secondary_text: styles.button_secondary_text || "#ffffff",
138
+ border_radius: styles.button_border_radius || "8px",
139
+ },
140
+ disable_scroll_animations_mobile: styles.disable_scroll_animations_mobile ?? false,
141
+ };
142
+
143
+ return NextResponse.json({ styles: transformed });
144
+ } catch (error) {
145
+ logger.error("[Admin:Styles]", "Failed to fetch styles:", error);
146
+ return NextResponse.json(
147
+ { error: "Failed to fetch styles" },
148
+ { status: 500 }
149
+ );
150
+ }
151
+ }
152
+
153
+ export async function POST(request: NextRequest) {
154
+ const authenticated = await isAdminAuthenticated();
155
+ if (!authenticated) {
156
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
157
+ }
158
+ if (!validateCsrf(request)) {
159
+ return csrfErrorResponse();
160
+ }
161
+ if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
162
+ return NextResponse.json({ error: "Request body too large" }, { status: 413 });
163
+ }
164
+
165
+ try {
166
+ const body = await request.json();
167
+ const { section, data } = body;
168
+
169
+ if (!section || !data) {
170
+ return NextResponse.json(
171
+ { error: "Missing section or data" },
172
+ { status: 400 }
173
+ );
174
+ }
175
+
176
+ const validSections = ["grid", "fonts", "typography", "colors", "links"];
177
+ if (!validSections.includes(section)) {
178
+ return NextResponse.json(
179
+ { error: `Invalid section: ${section}` },
180
+ { status: 400 }
181
+ );
182
+ }
183
+
184
+ let patch: Record<string, unknown> = {};
185
+
186
+ switch (section) {
187
+ case "grid": {
188
+ for (const key of ["grid_width", "grid_outer_padding", "grid_gutter_desktop", "grid_gutter_responsive", "grid_gutter_phone"]) {
189
+ if (data[key] !== undefined) {
190
+ patch[key] = data[key];
191
+ }
192
+ }
193
+ break;
194
+ }
195
+
196
+ case "fonts": {
197
+ interface FontInput {
198
+ _key?: string;
199
+ family: string;
200
+ is_builtin?: boolean;
201
+ variants?: VariantInput[];
202
+ }
203
+ interface VariantInput {
204
+ _key?: string;
205
+ weight?: string;
206
+ style?: string;
207
+ file_url?: string;
208
+ file_id?: string;
209
+ original_filename?: string;
210
+ }
211
+ patch = {
212
+ fonts: (data.fonts || []).map(
213
+ (font: FontInput) => ({
214
+ _key: font._key || crypto.randomUUID().slice(0, 8),
215
+ _type: "object",
216
+ family: font.family,
217
+ is_builtin: font.is_builtin || false,
218
+ variants: (font.variants || []).map(
219
+ (v: VariantInput) => ({
220
+ _key: v._key || crypto.randomUUID().slice(0, 8),
221
+ _type: "object",
222
+ weight: v.weight || "400",
223
+ style: v.style || "normal",
224
+ file_url: v.file_url || "",
225
+ file_id: v.file_id || "",
226
+ original_filename: v.original_filename || "",
227
+ })
228
+ ),
229
+ })
230
+ ),
231
+ };
232
+ break;
233
+ }
234
+
235
+ case "typography": {
236
+ // Data contains typography_h1, typography_h2, etc.
237
+ for (const level of ["h1", "h2", "h3", "h4", "body", "small"]) {
238
+ const key = `typography_${level}`;
239
+ if (data[key]) {
240
+ patch[key] = data[key];
241
+ }
242
+ }
243
+ break;
244
+ }
245
+
246
+ case "colors": {
247
+ // New palette-based colors
248
+ if (data.color_palette !== undefined) {
249
+ interface SwatchInput { _key?: string; _type?: string; name?: string; hex?: string }
250
+ patch.color_palette = (data.color_palette as SwatchInput[]).map((s: SwatchInput) => ({
251
+ _key: s._key || crypto.randomUUID().slice(0, 8),
252
+ _type: "object",
253
+ name: s.name || "Unnamed",
254
+ hex: s.hex || "#000000",
255
+ }));
256
+ }
257
+ // Legacy fixed color fields (backward compat)
258
+ for (const key of ["color_background", "color_text", "color_primary", "color_secondary", "color_accent", "color_muted"]) {
259
+ if (data[key] !== undefined) {
260
+ patch[key] = data[key];
261
+ }
262
+ }
263
+ break;
264
+ }
265
+
266
+ case "links": {
267
+ for (const key of [
268
+ "link_color", "link_hover_color", "link_underline",
269
+ "button_primary_bg", "button_primary_text",
270
+ "button_secondary_bg", "button_secondary_text",
271
+ "button_border_radius",
272
+ ]) {
273
+ if (data[key] !== undefined) {
274
+ patch[key] = data[key];
275
+ }
276
+ }
277
+ break;
278
+ }
279
+
280
+ case "animations": {
281
+ if (data.disable_scroll_animations_mobile !== undefined) {
282
+ patch.disable_scroll_animations_mobile = !!data.disable_scroll_animations_mobile;
283
+ }
284
+ break;
285
+ }
286
+ }
287
+
288
+ // Ensure the document exists
289
+ await writeClient.createIfNotExists({
290
+ _id: STYLES_ID,
291
+ _type: "siteStyles",
292
+ });
293
+
294
+ await writeClient.patch(STYLES_ID).set(patch).commit();
295
+
296
+ return NextResponse.json({ success: true, section });
297
+ } catch (error) {
298
+ logger.error("[Admin:Styles]", "Failed to save styles:", error);
299
+ return NextResponse.json(
300
+ { error: "Failed to save styles" },
301
+ { status: 500 }
302
+ );
303
+ }
304
+ }
@@ -0,0 +1,98 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getCachedProviderConfig } from "../../../../lib/storage";
3
+ import { logger } from "../../../../lib/logger";
4
+
5
+ /**
6
+ * GET /api/assets/[...path] — Provider-aware asset redirect
7
+ *
8
+ * Checks the active storage provider and redirects to the appropriate CDN.
9
+ *
10
+ * Currently only R2 is supported:
11
+ * - **R2:** 302 redirect to direct R2 public URL (no API call, no auth).
12
+ * Cache the redirect aggressively since R2 URLs are permanent.
13
+ *
14
+ * The provider is read from the assetRegistry in Sanity, cached in-memory
15
+ * for 5 minutes (lib/storage getCachedProviderConfig).
16
+ *
17
+ * Cache strategy (R2):
18
+ * - CDN: 24 hours (R2 URLs are permanent, only change if bucket URL changes)
19
+ * - Browser: 12 hours + stale-while-revalidate 12 hours
20
+ * - Effectively near-infinite cache; Cloudflare CDN handles the real caching
21
+ */
22
+
23
+ export async function GET(
24
+ request: NextRequest,
25
+ { params }: { params: Promise<{ path: string[] }> }
26
+ ) {
27
+ try {
28
+ const { path } = await params;
29
+ const relativePath = path.join("/");
30
+
31
+ if (!relativePath) {
32
+ return NextResponse.json({ error: "Path required" }, { status: 400 });
33
+ }
34
+
35
+ // ── Check active provider (cached 5min) ──
36
+ const config = await getCachedProviderConfig();
37
+
38
+ // ── Texture mode: stream bytes instead of redirecting ──
39
+ // Used by ShaderCanvas (WebGL) which needs same-origin image data.
40
+ // Cross-origin 302 redirects fail when the target lacks CORS headers.
41
+ // ?texture=1 fetches the image server-side and streams it back.
42
+ const isTextureMode = request.nextUrl.searchParams.get("texture") === "1";
43
+
44
+ // ── R2: direct redirect to public URL ──
45
+ if (config.r2BucketUrl) {
46
+ const r2Url = `${config.r2BucketUrl}/${relativePath}`;
47
+
48
+ // Texture mode: fetch from R2 server-side and stream bytes back
49
+ if (isTextureMode) {
50
+ const r2Resp = await fetch(r2Url);
51
+ if (!r2Resp.ok) {
52
+ return NextResponse.json(
53
+ { error: "Asset not found" },
54
+ { status: r2Resp.status }
55
+ );
56
+ }
57
+ const contentType = r2Resp.headers.get("content-type") || "image/png";
58
+ return new NextResponse(r2Resp.body, {
59
+ status: 200,
60
+ headers: {
61
+ "Content-Type": contentType,
62
+ "Access-Control-Allow-Origin": "*",
63
+ // Short cache — this path is only for WebGL textures (rare)
64
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600",
65
+ },
66
+ });
67
+ }
68
+
69
+ return NextResponse.redirect(r2Url, {
70
+ status: 302,
71
+ headers: {
72
+ // R2 URLs are permanent — cache aggressively.
73
+ // Vercel CDN: 24 hours
74
+ "CDN-Cache-Control": "public, s-maxage=86400",
75
+ // #21: Added must-revalidate so browsers check back after provider switches
76
+ "Cache-Control":
77
+ "public, max-age=43200, must-revalidate, stale-while-revalidate=43200",
78
+ // #21: ETag based on provider for cache busting on switch
79
+ "ETag": `"r2-${relativePath}"`,
80
+ },
81
+ });
82
+ }
83
+
84
+ // No provider configured
85
+ return NextResponse.json(
86
+ { error: "Storage provider not configured" },
87
+ { status: 500 }
88
+ );
89
+ } catch (err) {
90
+ logger.error("[AssetProxy]", "Asset proxy error:", err);
91
+
92
+ // Transient error — 503 with Retry-After hint
93
+ return NextResponse.json(
94
+ { error: "Failed to fetch asset" },
95
+ { status: 503, headers: { "Retry-After": "5" } }
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,43 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { client } from "../../../../lib/sanity/client";
3
+ import { customSectionByIdQuery } from "../../../../lib/sanity/queries";
4
+ import { logger } from "../../../../lib/logger";
5
+
6
+ type RouteContext = { params: Promise<{ id: string }> };
7
+
8
+ /**
9
+ * GET /api/custom-sections/[id] — Public: section data only (cached 1h CDN)
10
+ *
11
+ * Returns only the `section` field (PageSectionV2 data) for rendering
12
+ * custom section instances on the public site.
13
+ */
14
+ export async function GET(_request: NextRequest, context: RouteContext) {
15
+ try {
16
+ const { id } = await context.params;
17
+
18
+ if (!id || typeof id !== "string") {
19
+ return NextResponse.json({ error: "Missing section ID" }, { status: 400 });
20
+ }
21
+
22
+ const result = await client.fetch(customSectionByIdQuery, { id });
23
+ if (!result?.section) {
24
+ return NextResponse.json({ error: "Custom section not found" }, { status: 404 });
25
+ }
26
+
27
+ return NextResponse.json(
28
+ { section: result.section },
29
+ {
30
+ headers: {
31
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=1800",
32
+ "CDN-Cache-Control": "public, s-maxage=3600",
33
+ },
34
+ }
35
+ );
36
+ } catch (err) {
37
+ logger.error("[Public:CustomSections]", "Failed to fetch custom section", err);
38
+ return NextResponse.json(
39
+ { error: "Failed to fetch custom section" },
40
+ { status: 500 }
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,10 @@
1
+ import { draftMode } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export async function GET() {
5
+ const draft = await draftMode();
6
+ draft.disable();
7
+ return NextResponse.redirect(new URL("/", process.env.NEXT_PUBLIC_VERCEL_URL
8
+ ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
9
+ : "http://localhost:3000"));
10
+ }
@@ -0,0 +1,26 @@
1
+ import { defineEnableDraftMode } from "next-sanity/draft-mode";
2
+ import { client } from "../../../../lib/sanity/client";
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { validateAdminToken } from "../../../../lib/auth-token";
5
+
6
+ // Create the Sanity draft mode handler
7
+ const sanityDraftMode = defineEnableDraftMode({
8
+ client: client.withConfig({
9
+ token: process.env.SANITY_API_TOKEN,
10
+ }),
11
+ });
12
+
13
+ /**
14
+ * GET /api/draft-mode/enable — Enable Sanity draft mode.
15
+ * Defense-in-depth: requires admin auth in addition to Sanity token.
16
+ */
17
+ export async function GET(request: NextRequest) {
18
+ // Explicit admin auth check (defense-in-depth alongside Sanity token)
19
+ const adminToken = request.cookies.get("admin_token")?.value;
20
+ if (!adminToken || !(await validateAdminToken(adminToken))) {
21
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
22
+ }
23
+
24
+ // Delegate to Sanity's draft mode handler
25
+ return sanityDraftMode.GET(request);
26
+ }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from "next/server";
2
+ import { client } from "../../../lib/sanity/client";
3
+ import { publishedProjectSummariesQuery } from "../../../lib/sanity/queries";
4
+ import { logger } from "../../../lib/logger";
5
+
6
+ /**
7
+ * GET /api/projects — Public endpoint for published project summaries.
8
+ *
9
+ * Returns lightweight project data (title, slug, thumbnail_path, cover_video, seo_description)
10
+ * for use by public-site components like ProjectGridBlock.
11
+ *
12
+ * No authentication required — only returns published (non-draft) projects.
13
+ *
14
+ * Cache: ISR-managed via `revalidate` segment config (1 hour default).
15
+ * On-demand purge via `revalidatePath('/api/projects')` when projects are saved.
16
+ * Browser cache kept short (5 min) so users see updates quickly after ISR purge.
17
+ */
18
+
19
+ // ISR: Next.js manages the cache — revalidatePath('/api/projects') purges it on demand
20
+ export const revalidate = 3600; // 1 hour default
21
+
22
+ export async function GET() {
23
+ try {
24
+ const projects = await client.fetch(publishedProjectSummariesQuery);
25
+
26
+ return NextResponse.json(
27
+ { projects: projects || [] },
28
+ {
29
+ headers: {
30
+ // Browser-only cache (short) — the ISR/CDN layer is managed by Next.js via `revalidate`
31
+ "Cache-Control": "public, max-age=300",
32
+ },
33
+ }
34
+ );
35
+ } catch (error) {
36
+ logger.error("[Projects]", "[Public Projects API] Error:", error);
37
+ return NextResponse.json(
38
+ { projects: [] },
39
+ { status: 500 }
40
+ );
41
+ }
42
+ }
@@ -0,0 +1,88 @@
1
+ import { NextResponse } from "next/server";
2
+ import { client } from "../../../lib/sanity/client";
3
+ import { siteStylesQuery } from "../../../lib/sanity/queries";
4
+ import { logger } from "../../../lib/logger";
5
+ import { DEFAULT_GRID_WIDTH } from "../../../lib/builder/constants";
6
+ import { getSiteConfig } from "../../../lib/config";
7
+
8
+ /**
9
+ * GET /api/styles — Public endpoint for site styles (no auth).
10
+ * Used by StylesProvider to inject @font-face + CSS custom properties.
11
+ * Cached for 24 hours on CDN, serves stale for up to 1 hour while revalidating.
12
+ */
13
+
14
+ export async function GET() {
15
+ try {
16
+ const raw = await client.fetch(siteStylesQuery);
17
+
18
+ if (!raw) {
19
+ return NextResponse.json({ styles: null });
20
+ }
21
+
22
+ const cfg = getSiteConfig();
23
+
24
+ // Transform flat fields → nested SiteStyles
25
+ const styles = {
26
+ grid: {
27
+ width: raw.grid_width || DEFAULT_GRID_WIDTH,
28
+ outer_padding: raw.grid_outer_padding || "30",
29
+ gutter_desktop: raw.grid_gutter_desktop || "30",
30
+ gutter_responsive: raw.grid_gutter_responsive || "30",
31
+ gutter_phone: raw.grid_gutter_phone || "16",
32
+ },
33
+ fonts: raw.fonts || [],
34
+ typography: {
35
+ h1: raw.typography_h1 || null,
36
+ h2: raw.typography_h2 || null,
37
+ h3: raw.typography_h3 || null,
38
+ h4: raw.typography_h4 || null,
39
+ body: raw.typography_body || null,
40
+ small: raw.typography_small || null,
41
+ },
42
+ colors: {
43
+ swatches: (raw.color_palette || []).map((s: { _key?: string; name?: string; hex?: string }) => ({
44
+ _key: s._key || "",
45
+ name: s.name || "",
46
+ hex: s.hex || "#000000",
47
+ })),
48
+ // Legacy fields — config palette as fallback
49
+ background: raw.color_background || cfg.palette.background,
50
+ text: raw.color_text || cfg.palette.text,
51
+ primary: raw.color_primary || cfg.palette.primary,
52
+ secondary: raw.color_secondary || cfg.palette.secondary,
53
+ accent: raw.color_accent || cfg.palette.accent,
54
+ muted: raw.color_muted || cfg.palette.muted,
55
+ },
56
+ spacing: {
57
+ section_padding: raw.spacing_section_padding || "80px",
58
+ content_gap: raw.spacing_content_gap || "24px",
59
+ border_radius: raw.spacing_border_radius || "8px",
60
+ },
61
+ link_style: {
62
+ color: raw.link_color || cfg.palette.primary,
63
+ hover_color: raw.link_hover_color || cfg.palette.primary,
64
+ underline: raw.link_underline ?? true,
65
+ },
66
+ button_style: {
67
+ primary_bg: raw.button_primary_bg || "#ffffff",
68
+ primary_text: raw.button_primary_text || "#000000",
69
+ secondary_bg: raw.button_secondary_bg || "#1a1a1a",
70
+ secondary_text: raw.button_secondary_text || "#ffffff",
71
+ border_radius: raw.button_border_radius || "8px",
72
+ },
73
+ disable_scroll_animations_mobile: raw.disable_scroll_animations_mobile ?? false,
74
+ };
75
+
76
+ return NextResponse.json(
77
+ { styles },
78
+ {
79
+ headers: {
80
+ "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=3600",
81
+ },
82
+ }
83
+ );
84
+ } catch (error) {
85
+ logger.error("[Styles]", "Failed to fetch site styles:", error);
86
+ return NextResponse.json({ styles: null }, { status: 500 });
87
+ }
88
+ }
Binary file
@@ -0,0 +1,7 @@
1
+ /* Morphika instance — imports all core styles.
2
+ This is the monolith entry point; equivalent to:
3
+ @import "@morphika/webframe/styles/globals.css";
4
+ but uses relative paths since we're inside the monorepo. */
5
+ @import "../packages/core/styles/base.css";
6
+ @import "../packages/core/styles/animations.css";
7
+ @import "../packages/core/styles/admin.css";
package/app/layout.tsx ADDED
@@ -0,0 +1,53 @@
1
+ import type { Metadata } from "next";
2
+ import { VisualEditing } from "next-sanity/visual-editing";
3
+ import { draftMode } from "next/headers";
4
+ import { registerConfig, getSiteConfig } from "../lib/config";
5
+ import siteConfig from "../site.config";
6
+ import "./globals.css";
7
+
8
+ // Register config at module load time — before any component calls getSiteConfig().
9
+ // In the monolith this is redundant (static fallback exists), but it establishes
10
+ // the pattern that instances will use when consuming @morphika/webframe as a package.
11
+ registerConfig(siteConfig);
12
+
13
+ const cfg = getSiteConfig();
14
+
15
+ export const metadata: Metadata = {
16
+ title: {
17
+ default: cfg.defaults.metaTitle,
18
+ template: `%s — ${cfg.name}`,
19
+ },
20
+ description: cfg.defaults.metaDescription,
21
+ metadataBase: new URL(cfg.domain),
22
+ openGraph: {
23
+ title: cfg.defaults.metaTitle,
24
+ description: cfg.defaults.metaDescription,
25
+ siteName: cfg.name,
26
+ type: "website",
27
+ url: cfg.domain,
28
+ },
29
+ twitter: {
30
+ card: "summary_large_image",
31
+ title: cfg.defaults.metaTitle,
32
+ description: cfg.defaults.metaDescription,
33
+ },
34
+ };
35
+
36
+ export default async function RootLayout({
37
+ children,
38
+ }: Readonly<{
39
+ children: React.ReactNode;
40
+ }>) {
41
+ const draft = await draftMode();
42
+
43
+ return (
44
+ <html lang={cfg.lang || "en"} className="h-full antialiased">
45
+ <body className="min-h-full flex flex-col">
46
+ {children}
47
+ {draft.isEnabled && (
48
+ <VisualEditing />
49
+ )}
50
+ </body>
51
+ </html>
52
+ );
53
+ }