@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,122 @@
1
+ /**
2
+ * SiteConfig — Central configuration interface for the builder product.
3
+ *
4
+ * Each deployment has its own `site.config.ts` at the project root.
5
+ * All branding, defaults, and feature toggles live here instead of
6
+ * being hardcoded throughout the codebase.
7
+ */
8
+
9
+ export interface SiteConfig {
10
+ // ── Identity ──
11
+ /** Site name (used in metadata, admin sidebar, fallback heading) */
12
+ name: string;
13
+ /** Production domain (used in metadataBase, JSDoc examples) */
14
+ domain: string;
15
+ /** Admin panel title shown on the login page */
16
+ adminTitle: string;
17
+ /** HTML lang attribute (default: "en") */
18
+ lang?: string;
19
+
20
+ // ── Sanity ──
21
+ sanity: {
22
+ /** Sanity Studio project name identifier */
23
+ projectName: string;
24
+ /** Sanity Studio title shown in the UI */
25
+ studioTitle: string;
26
+ /** Labels for the Sanity Studio sidebar. English defaults used if omitted. */
27
+ studioLabels?: {
28
+ /** Root list title (default: "Content") */
29
+ content?: string;
30
+ /** Master pages group (default: "Master Pages") */
31
+ masterPages?: string;
32
+ /** Projects list (default: "Projects") */
33
+ projects?: string;
34
+ /** All pages list (default: "All Pages") */
35
+ allPages?: string;
36
+ /** Site settings label (default: "Site Settings") */
37
+ siteSettings?: string;
38
+ /** Asset registry label (default: "Asset Registry") */
39
+ assetRegistry?: string;
40
+ };
41
+ };
42
+
43
+ // ── Defaults (overrideable in admin) ──
44
+ defaults: {
45
+ /** Default logo text for the navbar */
46
+ logoText: string;
47
+ /** Default meta title for SEO */
48
+ metaTitle: string;
49
+ /** Default meta description for SEO */
50
+ metaDescription: string;
51
+ };
52
+
53
+ // ── Brand palette (fallback before admin configures styles) ──
54
+ palette: {
55
+ /** Page background */
56
+ background: string;
57
+ /** Default text color */
58
+ text: string;
59
+ /** Primary brand color (cursor border, buttons, links) */
60
+ primary: string;
61
+ /** Secondary brand color (cursor fill, accents) */
62
+ secondary: string;
63
+ /** Accent color (headings, highlights) */
64
+ accent: string;
65
+ /** Muted/gray for captions, subtle text */
66
+ muted: string;
67
+ /** Alternative accent (mapped to --color-brand-accent). Falls back to accent if omitted. */
68
+ accentAlt?: string;
69
+ /** Second accent color (mapped to --color-brand-accent-2). Optional. */
70
+ accent2?: string;
71
+ /** Dark background (mapped to --color-brand-dark). Falls back to background if omitted. */
72
+ dark?: string;
73
+ };
74
+
75
+ // ── Typography fallback ──
76
+ typography: {
77
+ /** Default font family name (loaded via @font-face or admin upload) */
78
+ defaultFont: string;
79
+ /** Monospace font stack fallback */
80
+ monoFallback: string;
81
+ /** Sans-serif font stack fallback */
82
+ sansFallback: string;
83
+ /**
84
+ * Built-in font files shipped with the instance (in public/fonts/).
85
+ * These are injected as @font-face rules by StylesProvider before
86
+ * admin styles load, preventing FOUT on first render.
87
+ * If omitted, only admin-uploaded fonts are available.
88
+ */
89
+ builtinFonts?: Array<{
90
+ family: string;
91
+ src: string;
92
+ weight?: string;
93
+ style?: string;
94
+ }>;
95
+ };
96
+
97
+ // ── Feature toggles ──
98
+ features: {
99
+ /** Show custom cursor on desktop (public site only) */
100
+ customCursor: boolean;
101
+ /** Enable portfolio/campaign visit tracking */
102
+ portfolioTracking: boolean;
103
+ /** Enable page exit animations (reverse enter/load animations on navigate) */
104
+ pageExitAnimations: boolean;
105
+ /** Disable scroll animations on mobile (< 768px) */
106
+ mobileAnimationDisable: boolean;
107
+ /** Enable WebGL shader effects on images (public site only) */
108
+ shaderEffects: boolean;
109
+ };
110
+
111
+ // ── Tracking (optional) ──
112
+ tracking: {
113
+ /** Knock API URL for visit tracking. Empty = tracking disabled. */
114
+ knockApiUrl: string;
115
+ /** Prefix for sessionStorage keys used by the tracker */
116
+ sessionPrefix: string;
117
+ };
118
+
119
+ // ── Storage keys prefix ──
120
+ /** Prefix for all localStorage/sessionStorage keys (canvas state, preferences, etc.) */
121
+ storagePrefix: string;
122
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, type ReactNode } from "react";
4
+ import {
5
+ assetUrl as defaultAssetUrl,
6
+ adminAssetUrl,
7
+ thumbUrl as defaultThumbUrl,
8
+ adminThumbUrl,
9
+ } from "../../lib/assets";
10
+
11
+ /**
12
+ * AssetContext — allows overriding how asset paths resolve to URLs.
13
+ *
14
+ * Three modes:
15
+ *
16
+ * - **default:** Uses `assetUrl()` / `thumbUrl()`. Provider-aware:
17
+ * - If `NEXT_PUBLIC_R2_BUCKET_URL` is set → direct R2 CDN URLs
18
+ * - Otherwise → `/api/assets/...` proxy (resolves provider at runtime)
19
+ *
20
+ * - **admin:** Uses `adminAssetUrl()` / `adminThumbUrl()`. Routes through
21
+ * `/api/admin/assets/file?path=...` which requires auth and handles
22
+ * provider detection internally (redirects to active provider).
23
+ *
24
+ * - **r2-direct:** Uses `assetUrl()` (same as default). Useful for admin
25
+ * preview when R2 is active and NEXT_PUBLIC_R2_BUCKET_URL is set —
26
+ * bypasses the admin proxy for direct CDN URLs. Falls back to proxy
27
+ * if the env var isn't set (same as default mode).
28
+ *
29
+ * `useThumbUrl()` returns the corresponding thumbnail resolver for each mode.
30
+ */
31
+
32
+ type AssetResolver = (path: string | undefined | null) => string;
33
+
34
+ interface AssetResolvers {
35
+ resolveAsset: AssetResolver;
36
+ resolveThumb: AssetResolver;
37
+ }
38
+
39
+ const AssetContext = createContext<AssetResolvers>({
40
+ resolveAsset: defaultAssetUrl,
41
+ resolveThumb: defaultThumbUrl,
42
+ });
43
+
44
+ /** Returns the full-resolution asset URL resolver */
45
+ export function useAssetUrl(): AssetResolver {
46
+ return useContext(AssetContext).resolveAsset;
47
+ }
48
+
49
+ /** Returns the thumbnail URL resolver */
50
+ export function useThumbUrl(): AssetResolver {
51
+ return useContext(AssetContext).resolveThumb;
52
+ }
53
+
54
+ export function AssetProvider({
55
+ mode,
56
+ children,
57
+ }: {
58
+ mode: "default" | "admin" | "r2-direct";
59
+ children: ReactNode;
60
+ }) {
61
+ const resolvers: AssetResolvers = (() => {
62
+ switch (mode) {
63
+ case "admin":
64
+ return { resolveAsset: adminAssetUrl, resolveThumb: adminThumbUrl };
65
+ case "r2-direct":
66
+ // Use default resolvers (which check NEXT_PUBLIC_R2_BUCKET_URL first).
67
+ // If the env var is set → direct R2 CDN URLs.
68
+ // If not → falls through to proxy (same as "default").
69
+ return { resolveAsset: defaultAssetUrl, resolveThumb: defaultThumbUrl };
70
+ case "default":
71
+ default:
72
+ return { resolveAsset: defaultAssetUrl, resolveThumb: defaultThumbUrl };
73
+ }
74
+ })();
75
+
76
+ return (
77
+ <AssetContext.Provider value={resolvers}>{children}</AssetContext.Provider>
78
+ );
79
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NavAnimationContext — Per-page nav entrance animation override.
5
+ *
6
+ * Same pattern as NavColorContext: the Navbar reads from this context,
7
+ * and each page can push overrides via the PageNavAnimation component.
8
+ *
9
+ * Session 115: Created for per-page nav entrance animation overrides.
10
+ */
11
+
12
+ import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
13
+ import type { NavEntrancePreset } from "../../lib/sanity/types";
14
+
15
+ interface NavAnimationOverride {
16
+ preset?: NavEntrancePreset;
17
+ duration?: number;
18
+ delay?: number;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ interface NavAnimationContextValue {
23
+ override: NavAnimationOverride;
24
+ setOverride: (o: NavAnimationOverride) => void;
25
+ }
26
+
27
+ const NavAnimationContext = createContext<NavAnimationContextValue>({
28
+ override: {},
29
+ setOverride: () => {},
30
+ });
31
+
32
+ export function NavAnimationProvider({ children }: { children: ReactNode }) {
33
+ const [override, setOverrideState] = useState<NavAnimationOverride>({});
34
+ const setOverride = useCallback((o: NavAnimationOverride) => setOverrideState(o), []);
35
+ return (
36
+ <NavAnimationContext.Provider value={{ override, setOverride }}>
37
+ {children}
38
+ </NavAnimationContext.Provider>
39
+ );
40
+ }
41
+
42
+ export function useNavAnimation() {
43
+ return useContext(NavAnimationContext);
44
+ }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState, useCallback } from "react";
4
+
5
+ interface NavColorContextType {
6
+ /** Hex color string (e.g. "#ffffff") or empty for default */
7
+ navColor: string;
8
+ setNavColor: (color: string) => void;
9
+ }
10
+
11
+ const NavColorContext = createContext<NavColorContextType>({
12
+ navColor: "",
13
+ setNavColor: () => {},
14
+ });
15
+
16
+ export function NavColorProvider({
17
+ children,
18
+ initialColor = "",
19
+ }: {
20
+ children: React.ReactNode;
21
+ initialColor?: string;
22
+ }) {
23
+ const [navColor, setNavColorState] = useState<string>(initialColor);
24
+
25
+ const setNavColor = useCallback((color: string) => {
26
+ setNavColorState(color);
27
+ }, []);
28
+
29
+ return (
30
+ <NavColorContext.Provider value={{ navColor, setNavColor }}>
31
+ {children}
32
+ </NavColorContext.Provider>
33
+ );
34
+ }
35
+
36
+ export function useNavColor() {
37
+ return useContext(NavColorContext);
38
+ }
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageExitContext — intercepts internal link clicks, plays reverse exit
5
+ * animations on enter/load-triggered elements, then navigates.
6
+ *
7
+ * How it works:
8
+ * 1. A capture-phase click handler on the site container intercepts internal <a> clicks
9
+ * BEFORE Next.js Link processes them (capture fires before bubble).
10
+ * 2. For each animated element (enter/load trigger), the current animation is reset
11
+ * and replayed in reverse via animation-direction: reverse.
12
+ * 3. After the exit duration (capped at 600ms), router.push() fires the navigation.
13
+ *
14
+ * Skipped when:
15
+ * - prefers-reduced-motion is set
16
+ * - No animated elements exist on the page
17
+ * - Click originated from the mobile menu overlay (instant nav feels better)
18
+ * - Modifier keys held (Cmd+click → new tab)
19
+ * - Already exiting
20
+ */
21
+
22
+ import {
23
+ createContext,
24
+ useContext,
25
+ useRef,
26
+ useState,
27
+ useEffect,
28
+ useCallback,
29
+ type ReactNode,
30
+ } from "react";
31
+ import { useRouter, usePathname } from "next/navigation";
32
+
33
+ interface PageExitContextValue {
34
+ /** True while exit animations are playing before navigation */
35
+ isExiting: boolean;
36
+ }
37
+
38
+ const PageExitContext = createContext<PageExitContextValue>({ isExiting: false });
39
+
40
+ export function usePageExit() {
41
+ return useContext(PageExitContext);
42
+ }
43
+
44
+ // Selector for elements with completed enter animations (new system: data-entered)
45
+ const ANIMATED_SELECTOR = '[data-entered]';
46
+
47
+ export function PageExitProvider({ children }: { children: ReactNode }) {
48
+ const router = useRouter();
49
+ const pathname = usePathname();
50
+ const [isExiting, setIsExiting] = useState(false);
51
+ const exitingRef = useRef(false);
52
+ const containerRef = useRef<HTMLDivElement>(null);
53
+
54
+ // Reset exiting state when pathname changes (navigation completed)
55
+ useEffect(() => {
56
+ exitingRef.current = false;
57
+ setIsExiting(false);
58
+ }, [pathname]);
59
+
60
+ /**
61
+ * Trigger exit animations on all enter/load elements, then navigate.
62
+ */
63
+ const triggerExit = useCallback(
64
+ (href: string) => {
65
+ if (exitingRef.current) return;
66
+
67
+ // Respect prefers-reduced-motion
68
+ if (
69
+ typeof window !== "undefined" &&
70
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
71
+ ) {
72
+ router.push(href);
73
+ return;
74
+ }
75
+
76
+ // Find animated elements eligible for exit
77
+ const elements = document.querySelectorAll<HTMLElement>(ANIMATED_SELECTOR);
78
+
79
+ if (elements.length === 0) {
80
+ router.push(href);
81
+ return;
82
+ }
83
+
84
+ exitingRef.current = true;
85
+ setIsExiting(true);
86
+
87
+ let maxDuration = 0;
88
+
89
+ elements.forEach((el) => {
90
+ // Card entrance elements get a simple fade-out instead of reverse
91
+ // (reversing slide-up → slide-down looks wrong for grid cards)
92
+ const isCardEntrance = el.closest("[data-card-entrance]") !== null;
93
+
94
+ // Read current animation values (set as inline styles by React)
95
+ const name = el.style.animationName;
96
+ const dur = el.style.animationDuration || "600ms";
97
+ const timing = el.style.animationTimingFunction || "ease-out";
98
+
99
+ if (!name || name === "none") return;
100
+
101
+ const ms = parseFloat(dur); // React sets "600ms" format
102
+ if (ms > maxDuration) maxDuration = ms;
103
+
104
+ // Reset animation to force browser to re-trigger
105
+ el.style.animation = "none";
106
+ void el.offsetHeight; // force reflow
107
+
108
+ if (isCardEntrance) {
109
+ // Simple fade-out for card entrance elements
110
+ el.style.animationName = "enter-fade";
111
+ el.style.animationDuration = "400ms";
112
+ el.style.animationDirection = "reverse";
113
+ el.style.animationFillMode = "forwards";
114
+ el.style.animationTimingFunction = "ease-out";
115
+ el.style.animationDelay = "0ms";
116
+ } else {
117
+ // Standard reverse for all other animated elements
118
+ el.style.animationName = name;
119
+ el.style.animationDuration = dur;
120
+ el.style.animationDirection = "reverse";
121
+ el.style.animationFillMode = "forwards";
122
+ el.style.animationTimingFunction = timing;
123
+ el.style.animationDelay = "0ms";
124
+ }
125
+ });
126
+
127
+ // Navigate after exit completes — use 70% of duration (exits feel
128
+ // snappier than entrances), capped at 600ms.
129
+ const exitTime = Math.min(Math.max(maxDuration * 0.7, 200), 600);
130
+ setTimeout(() => {
131
+ router.push(href);
132
+ }, exitTime);
133
+ },
134
+ [router],
135
+ );
136
+
137
+ // ── Global click interceptor (capture phase) ──
138
+ useEffect(() => {
139
+ const container = containerRef.current;
140
+ if (!container) return;
141
+
142
+ const handleClick = (e: MouseEvent) => {
143
+ // Block link clicks while exit is in progress
144
+ if (exitingRef.current) {
145
+ const anchor = (e.target as HTMLElement).closest("a[href]");
146
+ if (anchor) e.preventDefault();
147
+ return;
148
+ }
149
+
150
+ const anchor = (e.target as HTMLElement).closest("a[href]");
151
+ if (!anchor) return;
152
+
153
+ const href = anchor.getAttribute("href");
154
+ if (!href) return;
155
+
156
+ // ── Skip non-internal links ──
157
+ if (
158
+ href.startsWith("http") ||
159
+ href.startsWith("#") ||
160
+ href.startsWith("mailto:") ||
161
+ href.startsWith("tel:")
162
+ )
163
+ return;
164
+ if (anchor.getAttribute("target") === "_blank") return;
165
+ if (href === pathname) return;
166
+ if (href.startsWith("/admin") || href.startsWith("/studio")) return;
167
+
168
+ // Skip if modifier keys (Cmd+click, Ctrl+click → new tab)
169
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
170
+
171
+ // Skip exit animations when clicking from mobile menu overlay —
172
+ // instant navigation feels better after the menu close transition.
173
+ if (anchor.closest("#mobile-nav-menu")) return;
174
+
175
+ // Only trigger if there are animated elements to exit
176
+ if (!document.querySelector(ANIMATED_SELECTOR)) return;
177
+
178
+ // Intercept: prevent Next.js Link from navigating immediately
179
+ e.preventDefault();
180
+ triggerExit(href);
181
+ };
182
+
183
+ // Capture phase ensures we fire BEFORE Next.js Link's bubble handler.
184
+ // Next.js Link checks event.defaultPrevented and bails if true.
185
+ container.addEventListener("click", handleClick, true);
186
+ return () => container.removeEventListener("click", handleClick, true);
187
+ }, [pathname, triggerExit]);
188
+
189
+ return (
190
+ <PageExitContext.Provider value={{ isExiting }}>
191
+ <div ref={containerRef}>{children}</div>
192
+ </PageExitContext.Provider>
193
+ );
194
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ThumbStatusContext — provides thumbnail availability data to builder components.
5
+ *
6
+ * Fetches the asset registry once and exposes a Map<path, boolean> lookup.
7
+ * Used by BlockLivePreview to show green/amber thumb badges on image blocks.
8
+ *
9
+ * Wrap around the builder canvas:
10
+ * <ThumbStatusProvider>
11
+ * <BuilderCanvas>...</BuilderCanvas>
12
+ * </ThumbStatusProvider>
13
+ */
14
+
15
+ import { createContext, useContext, useEffect, useState, useMemo, useCallback } from "react";
16
+ import type { ReactNode } from "react";
17
+ import { isRasterImage } from "../../lib/thumbnails/generate";
18
+
19
+ interface ThumbStatusContextValue {
20
+ /** Returns true if asset has a thumbnail, false if not, undefined if unknown/not raster */
21
+ hasThumb: (assetPath: string) => boolean | undefined;
22
+ /** Refresh registry data (call after uploads) */
23
+ refresh: () => void;
24
+ }
25
+
26
+ const ThumbStatusContext = createContext<ThumbStatusContextValue>({
27
+ hasThumb: () => undefined,
28
+ refresh: () => {},
29
+ });
30
+
31
+ export function useThumbStatus() {
32
+ return useContext(ThumbStatusContext);
33
+ }
34
+
35
+ interface RegistryEntry {
36
+ path: string;
37
+ has_thumbnail?: boolean;
38
+ status?: string;
39
+ }
40
+
41
+ export function ThumbStatusProvider({ children }: { children: ReactNode }) {
42
+ const [thumbMap, setThumbMap] = useState<Map<string, boolean>>(new Map());
43
+
44
+ const fetchRegistry = useCallback(async () => {
45
+ try {
46
+ const res = await fetch("/api/admin/assets/registry");
47
+ if (!res.ok) return;
48
+ const data = await res.json();
49
+ const assets: RegistryEntry[] = data.registry?.assets || [];
50
+ const map = new Map<string, boolean>();
51
+ for (const a of assets) {
52
+ if (a.status === "missing") continue;
53
+ map.set(a.path, a.has_thumbnail === true);
54
+ }
55
+ setThumbMap(map);
56
+ } catch {
57
+ // Silent — badges simply won't show
58
+ }
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ fetchRegistry();
63
+ }, [fetchRegistry]);
64
+
65
+ const hasThumb = useCallback(
66
+ (assetPath: string): boolean | undefined => {
67
+ if (!assetPath || !isRasterImage(assetPath)) return undefined;
68
+ return thumbMap.get(assetPath);
69
+ },
70
+ [thumbMap]
71
+ );
72
+
73
+ const value = useMemo(
74
+ () => ({ hasThumb, refresh: fetchRegistry }),
75
+ [hasThumb, fetchRegistry]
76
+ );
77
+
78
+ return (
79
+ <ThumbStatusContext.Provider value={value}>
80
+ {children}
81
+ </ThumbStatusContext.Provider>
82
+ );
83
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Client-side CSRF helpers.
3
+ *
4
+ * Reads the `csrf_token` cookie (set by the server on login and refreshed
5
+ * by middleware) and returns headers suitable for fetch() calls.
6
+ */
7
+
8
+ const CSRF_COOKIE_NAME = "csrf_token";
9
+ const CSRF_HEADER_NAME = "x-csrf-token";
10
+
11
+ /**
12
+ * Read the CSRF token from the browser cookie jar.
13
+ * Returns undefined if not found or running on the server.
14
+ */
15
+ export function getCsrfToken(): string | undefined {
16
+ if (typeof window === "undefined") return undefined;
17
+ // Use regex to safely extract the value, avoiding split pitfalls
18
+ const match = window.document.cookie.match(
19
+ new RegExp(`(?:^|;\\s*)${CSRF_COOKIE_NAME}=([^;]*)`)
20
+ );
21
+ return match?.[1] || undefined;
22
+ }
23
+
24
+ /**
25
+ * Build a headers object with the CSRF token included.
26
+ * Merge this into your fetch() headers for mutation requests.
27
+ *
28
+ * @example
29
+ * fetch(url, { method: "POST", headers: { ...csrfHeaders(), "Content-Type": "application/json" } })
30
+ */
31
+ export function csrfHeaders(): Record<string, string> {
32
+ const token = getCsrfToken();
33
+ return token ? { [CSRF_HEADER_NAME]: token } : {};
34
+ }
package/lib/csrf.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * CSRF protection using double-submit cookie pattern.
3
+ *
4
+ * How it works:
5
+ * 1. Server generates a random CSRF token and sets it as a cookie
6
+ * 2. Client reads the cookie and sends it as a header (X-CSRF-Token)
7
+ * 3. Server validates that the header matches the cookie
8
+ *
9
+ * Why this works: A cross-origin attacker can trigger the browser to send
10
+ * cookies, but cannot read them (SameSite + HttpOnly). Since the attacker
11
+ * can't read the cookie, they can't set the matching header.
12
+ *
13
+ * The CSRF cookie is NOT HttpOnly so the client can read and echo it.
14
+ * This is safe — the value is a random token with no sensitive info.
15
+ */
16
+
17
+ import { NextRequest, NextResponse } from "next/server";
18
+
19
+ const CSRF_COOKIE_NAME = "csrf_token";
20
+ const CSRF_HEADER_NAME = "x-csrf-token";
21
+ const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22
+
23
+ /**
24
+ * Generate a new CSRF token.
25
+ */
26
+ export function generateCsrfToken(): string {
27
+ return crypto.randomUUID();
28
+ }
29
+
30
+ /**
31
+ * Set the CSRF token cookie on a response.
32
+ * Called when the admin logs in or when no CSRF cookie exists.
33
+ */
34
+ export function setCsrfCookie(response: NextResponse, token: string): void {
35
+ response.cookies.set(CSRF_COOKIE_NAME, token, {
36
+ httpOnly: false, // Client must be able to read this
37
+ secure: process.env.NODE_ENV === "production",
38
+ sameSite: "strict",
39
+ path: "/",
40
+ maxAge: 60 * 60 * 24, // 24 hours
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Validate CSRF token on a mutation request.
46
+ * Compares the cookie value to the X-CSRF-Token header.
47
+ *
48
+ * Returns true if valid, false if CSRF check fails.
49
+ */
50
+ export function validateCsrf(request: NextRequest): boolean {
51
+ const cookieToken = request.cookies.get(CSRF_COOKIE_NAME)?.value;
52
+ const headerToken = request.headers.get(CSRF_HEADER_NAME);
53
+
54
+ if (!cookieToken || !headerToken) return false;
55
+ if (!UUID_V4_REGEX.test(cookieToken)) return false; // Must be valid UUID v4
56
+
57
+ return cookieToken === headerToken;
58
+ }
59
+
60
+ /**
61
+ * Return a 403 response for CSRF validation failure.
62
+ */
63
+ export function csrfErrorResponse(): NextResponse {
64
+ return NextResponse.json(
65
+ { error: "CSRF validation failed" },
66
+ { status: 403 }
67
+ );
68
+ }
@@ -0,0 +1,24 @@
1
+ // ============================================
2
+ // Shared Format Utilities
3
+ // ============================================
4
+ // Extracted from storage/page.tsx and assets/page.tsx (Session 131)
5
+
6
+ /**
7
+ * Format a date string to locale string, or "Never" if absent.
8
+ */
9
+ export function formatDate(date: string | undefined | null): string {
10
+ if (!date) return "Never";
11
+ return new Date(date).toLocaleString();
12
+ }
13
+
14
+ /**
15
+ * Format byte count to human-readable string (B, KB, MB, GB, TB).
16
+ * Uses logarithmic approach for precision at all magnitudes.
17
+ */
18
+ export function formatBytes(bytes: number | undefined): string {
19
+ if (!bytes || bytes === 0) return "0 B";
20
+ const units = ["B", "KB", "MB", "GB", "TB"];
21
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
22
+ const val = bytes / Math.pow(1024, i);
23
+ return `${val < 10 ? val.toFixed(2) : val < 100 ? val.toFixed(1) : Math.round(val)} ${units[i]}`;
24
+ }