@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,45 @@
1
+ /**
2
+ * RGB Shift (chromatic aberration) fragment shader.
3
+ *
4
+ * Samples R, G, B channels with slightly offset UVs.
5
+ * Offset direction follows cursor position (hover) or scroll progress (scroll).
6
+ *
7
+ * Uniforms:
8
+ * uTexture — image texture
9
+ * uMouse — normalized cursor/scroll position (0–1)
10
+ * uIntensity — config intensity (0.5–2.0)
11
+ * uSpeed — config speed (0.5–3.0) — unused for this preset
12
+ * uTime — elapsed time (seconds) — unused for this preset
13
+ * uHover — lerped hover/scroll state (0→1)
14
+ * uResolution — canvas size in pixels
15
+ *
16
+ * Phase 2 shader — included for completeness, activated in Session 72.
17
+ */
18
+ export const rgbShiftFragment = /* glsl */ `
19
+ precision highp float;
20
+
21
+ uniform sampler2D uTexture;
22
+ uniform vec2 uMouse;
23
+ uniform float uIntensity;
24
+ uniform float uSpeed;
25
+ uniform float uTime;
26
+ uniform float uHover;
27
+ uniform vec2 uResolution;
28
+
29
+ varying vec2 vUv;
30
+
31
+ void main() {
32
+ vec2 uv = vUv;
33
+
34
+ // Direction from center to mouse/scroll position
35
+ vec2 dir = uMouse - 0.5;
36
+ float strength = length(dir) * 0.02 * uIntensity * uHover;
37
+
38
+ // Offset each channel in a different direction
39
+ float r = texture2D(uTexture, uv + dir * strength).r;
40
+ float g = texture2D(uTexture, uv).g;
41
+ float b = texture2D(uTexture, uv - dir * strength).b;
42
+
43
+ gl_FragColor = vec4(r, g, b, 1.0);
44
+ }
45
+ `;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ripple distortion fragment shader.
3
+ *
4
+ * Circular wave distortion emanating from the cursor position.
5
+ * Trigger: hover only.
6
+ *
7
+ * Uniforms:
8
+ * uTexture — image texture
9
+ * uMouse — normalized cursor position (0–1)
10
+ * uIntensity — config intensity (0.5–2.0)
11
+ * uSpeed — config speed (0.5–3.0)
12
+ * uTime — elapsed time (seconds)
13
+ * uHover — lerped hover state (0→1)
14
+ * uResolution — canvas size in pixels
15
+ */
16
+ export const rippleFragment = /* glsl */ `
17
+ precision highp float;
18
+
19
+ uniform sampler2D uTexture;
20
+ uniform vec2 uMouse;
21
+ uniform float uIntensity;
22
+ uniform float uSpeed;
23
+ uniform float uTime;
24
+ uniform float uHover;
25
+ uniform vec2 uResolution;
26
+
27
+ varying vec2 vUv;
28
+
29
+ void main() {
30
+ vec2 uv = vUv;
31
+
32
+ // Distance from cursor
33
+ float dist = distance(uv, uMouse);
34
+
35
+ // Ripple wave — frequency and time-based animation
36
+ float ripple = sin(dist * 30.0 * uSpeed - uTime * 4.0) * 0.02 * uIntensity;
37
+ ripple *= smoothstep(0.5, 0.0, dist); // Fade with distance from cursor
38
+ ripple *= uHover; // Only active when hovering
39
+
40
+ // Displacement direction: push outward from cursor
41
+ vec2 direction = dist > 0.001 ? normalize(uv - uMouse) : vec2(0.0);
42
+ vec2 displaced = uv + direction * ripple;
43
+
44
+ gl_FragColor = texture2D(uTexture, displaced);
45
+ }
46
+ `;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared vertex shader — pass-through UV coordinates.
3
+ * Used by all shader presets.
4
+ */
5
+ export const vertexShader = /* glsl */ `
6
+ attribute vec2 uv;
7
+ attribute vec2 position;
8
+ varying vec2 vUv;
9
+
10
+ void main() {
11
+ vUv = uv;
12
+ gl_Position = vec4(position, 0, 1);
13
+ }
14
+ `;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Storage Provider Factory
3
+ *
4
+ * Central entry point for the storage abstraction layer.
5
+ * Reads the active provider from Sanity and returns the
6
+ * appropriate adapter instance.
7
+ *
8
+ * Currently only R2 is implemented. The adapter pattern is
9
+ * preserved so new providers can be added in the future.
10
+ *
11
+ * Usage:
12
+ * const adapter = await getStorageAdapter();
13
+ * const files = await adapter.listFiles();
14
+ * const url = await adapter.resolveUrl("projects/hero.jpg");
15
+ */
16
+
17
+ import { client } from "../../lib/sanity/client";
18
+ import { R2Adapter } from "./r2-adapter";
19
+ import { logger } from "../../lib/logger";
20
+ import type { StorageAdapter, StorageProvider, StorageProviderConfig } from "./types";
21
+
22
+ // ============================================
23
+ // Adapter singletons (reused across requests)
24
+ // ============================================
25
+
26
+ let r2Adapter: R2Adapter | null = null;
27
+
28
+ function getR2Adapter(): R2Adapter {
29
+ if (!r2Adapter) r2Adapter = new R2Adapter();
30
+ return r2Adapter;
31
+ }
32
+
33
+ // ============================================
34
+ // Active provider resolution
35
+ // ============================================
36
+
37
+ /**
38
+ * Read the active storage provider from the asset registry.
39
+ * Defaults to "r2". Preserved for multi-provider architecture.
40
+ */
41
+ export async function getActiveProvider(): Promise<StorageProvider> {
42
+ try {
43
+ const registry = await client.fetch(
44
+ `*[_type == "assetRegistry"][0]{ storage_provider }`
45
+ );
46
+ const provider = registry?.storage_provider;
47
+ if (provider === "r2") return "r2";
48
+ // Default to r2 (only provider currently supported)
49
+ return "r2";
50
+ } catch (err) {
51
+ logger.error("[Storage]", "Failed to read active provider from Sanity, falling back to R2", err);
52
+ return "r2";
53
+ }
54
+ }
55
+
56
+ // ============================================
57
+ // Factory
58
+ // ============================================
59
+
60
+ /**
61
+ * Get the storage adapter for the currently active provider.
62
+ *
63
+ * @param provider - Explicit provider override. If omitted, reads from Sanity.
64
+ * Use this when the caller already knows which provider to use
65
+ * (e.g., the storage admin page showing both providers).
66
+ */
67
+ export async function getStorageAdapter(
68
+ provider?: StorageProvider
69
+ ): Promise<StorageAdapter> {
70
+ const activeProvider = provider ?? (await getActiveProvider());
71
+
72
+ switch (activeProvider) {
73
+ case "r2":
74
+ default:
75
+ return getR2Adapter();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get a specific adapter by provider name (does not check active provider).
81
+ * Used by admin UI to interact with a non-active provider (e.g., testing
82
+ * a new provider connection while another is still active).
83
+ */
84
+ export function getAdapterForProvider(provider: StorageProvider): StorageAdapter {
85
+ switch (provider) {
86
+ case "r2":
87
+ return getR2Adapter();
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get the status of all configured storage providers.
93
+ * Returns status for each provider, plus which one is active.
94
+ */
95
+ export async function getAllProviderStatuses(): Promise<{
96
+ activeProvider: StorageProvider;
97
+ providers: StorageProviderConfig[];
98
+ }> {
99
+ const [activeResult, r2Result] = await Promise.allSettled([
100
+ getActiveProvider(),
101
+ getR2Adapter().getStatus(),
102
+ ]);
103
+
104
+ const activeProvider = activeResult.status === "fulfilled" ? activeResult.value : "r2";
105
+
106
+ const r2Status: StorageProviderConfig = r2Result.status === "fulfilled"
107
+ ? r2Result.value
108
+ : { provider: "r2", connected: false, displayName: "Cloudflare R2 — Error loading status" };
109
+
110
+ return {
111
+ activeProvider,
112
+ providers: [r2Status],
113
+ };
114
+ }
115
+
116
+ // ============================================
117
+ // Cached R2 public URL (for proxy routes)
118
+ // ============================================
119
+
120
+ /**
121
+ * In-memory cache for the R2 public URL.
122
+ * Used by the asset proxy routes to resolve R2 redirects without
123
+ * hitting Sanity on every request. TTL: 5 minutes.
124
+ */
125
+ let r2ConfigCache: {
126
+ provider: StorageProvider;
127
+ r2BucketUrl: string | null;
128
+ fetchedAt: number;
129
+ } | null = null;
130
+
131
+ // #4: In-flight promise deduplication — prevents cache stampede race condition
132
+ let inFlightFetch: Promise<ProviderConfig> | null = null;
133
+
134
+ const PROVIDER_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
135
+
136
+ export interface ProviderConfig {
137
+ provider: StorageProvider;
138
+ r2BucketUrl: string | null;
139
+ }
140
+
141
+ /**
142
+ * Get the active provider + config, with in-memory caching.
143
+ *
144
+ * Used by /api/assets and /api/admin/assets/file to decide
145
+ * how to redirect asset requests.
146
+ * Cached for 5 minutes to avoid hitting Sanity on every asset request.
147
+ *
148
+ * #4: Uses promise chaining to deduplicate concurrent requests.
149
+ * #23: Adds AbortController timeout on Sanity fetch.
150
+ */
151
+ export async function getCachedProviderConfig(): Promise<ProviderConfig> {
152
+ const now = Date.now();
153
+ if (r2ConfigCache && now - r2ConfigCache.fetchedAt < PROVIDER_CACHE_TTL_MS) {
154
+ return r2ConfigCache;
155
+ }
156
+
157
+ // #4: If there's already an in-flight fetch, reuse it instead of starting a new one
158
+ if (inFlightFetch) return inFlightFetch;
159
+
160
+ inFlightFetch = (async () => {
161
+ try {
162
+ // #23: Add 5-second timeout to prevent hanging on slow Sanity
163
+ const controller = new AbortController();
164
+ const timeout = setTimeout(() => controller.abort(), 5000);
165
+
166
+ const registry = await client.fetch(
167
+ `*[_type == "assetRegistry"][0]{
168
+ storage_provider,
169
+ r2_bucket_url
170
+ }`,
171
+ {},
172
+ { signal: controller.signal } as Record<string, unknown>
173
+ );
174
+
175
+ clearTimeout(timeout);
176
+
177
+ const config: ProviderConfig = {
178
+ provider: registry?.storage_provider === "r2" ? "r2" : "r2",
179
+ r2BucketUrl: registry?.r2_bucket_url || null,
180
+ };
181
+
182
+ r2ConfigCache = { ...config, fetchedAt: Date.now() };
183
+ return config;
184
+ } catch (err) {
185
+ // #19: Log errors for detectability
186
+ logger.error("[Storage]", "Failed to fetch provider config from Sanity", err);
187
+ // On error, return cached value if available, otherwise default to R2
188
+ if (r2ConfigCache) return r2ConfigCache as ProviderConfig;
189
+ return { provider: "r2" as StorageProvider, r2BucketUrl: null };
190
+ } finally {
191
+ inFlightFetch = null;
192
+ }
193
+ })();
194
+
195
+ return inFlightFetch;
196
+ }
197
+
198
+ /**
199
+ * Invalidate the provider config cache.
200
+ * Call this after switching providers or updating R2 configuration.
201
+ */
202
+ export function invalidateProviderConfigCache(): void {
203
+ r2ConfigCache = null;
204
+ }
205
+
206
+ // ============================================
207
+ // Re-exports for convenience
208
+ // ============================================
209
+
210
+ export type { StorageAdapter, StorageProvider, StorageProviderConfig, ScannedFile } from "./types";
211
+ export { getMimeType, isMediaFile, MEDIA_EXTENSIONS } from "./types";
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Cloudflare R2 Storage Adapter
3
+ *
4
+ * Implements the StorageAdapter interface for Cloudflare R2.
5
+ *
6
+ * R2 uses direct public URLs for resolveUrl() (no API call needed)
7
+ * and S3-compatible ListObjectsV2Command for file listing.
8
+ * Static API keys — no token refresh complexity.
9
+ */
10
+
11
+ import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
12
+ import { client } from "../../lib/sanity/client";
13
+ import { decryptToken } from "../../lib/security";
14
+ import { logger } from "../../lib/logger";
15
+ import type { StorageAdapter, StorageProviderConfig, ScannedFile } from "./types";
16
+ import { isMediaFile, getMimeType } from "./types";
17
+
18
+ // #18: Singleton S3Client — lazy initialized, reused across all operations
19
+ let s3Singleton: S3Client | null = null;
20
+ let s3ConfigHash: string = "";
21
+
22
+ // ============================================
23
+ // R2 configuration (read from Sanity)
24
+ // ============================================
25
+
26
+ interface R2Config {
27
+ bucketUrl: string; // Public URL (e.g. https://assets.example.com)
28
+ endpoint: string; // S3 endpoint (e.g. https://<id>.r2.cloudflarestorage.com)
29
+ bucketName: string; // Bucket name (e.g. my-assets)
30
+ accessKeyId: string; // Decrypted
31
+ secretAccessKey: string; // Decrypted
32
+ }
33
+
34
+ // ============================================
35
+ // Adapter implementation
36
+ // ============================================
37
+
38
+ export class R2Adapter implements StorageAdapter {
39
+ readonly provider = "r2" as const;
40
+
41
+ /**
42
+ * List all media files in the R2 bucket.
43
+ *
44
+ * Uses S3-compatible ListObjectsV2Command to paginate through all objects.
45
+ * Filters to supported media extensions only.
46
+ *
47
+ * @param rootPath - Optional key prefix to scan from (e.g. "projects/").
48
+ * Empty or undefined = scan entire bucket.
49
+ */
50
+ async listFiles(rootPath?: string): Promise<ScannedFile[]> {
51
+ const config = await this.getConfig();
52
+ if (!config.endpoint || !config.bucketName || !config.accessKeyId || !config.secretAccessKey) {
53
+ throw new Error(
54
+ "R2 not connected. Go to /admin/storage to connect your R2 bucket."
55
+ );
56
+ }
57
+
58
+ // #18: Reuse singleton S3Client instead of creating a new one per call
59
+ const s3 = this.getOrCreateS3Client(config);
60
+
61
+ // Normalize prefix: strip leading/trailing slashes, add trailing slash if non-empty
62
+ const prefix = rootPath
63
+ ? rootPath.replace(/^\/+|\/+$/g, "") + "/"
64
+ : undefined;
65
+
66
+ const files: ScannedFile[] = [];
67
+ let continuationToken: string | undefined;
68
+
69
+ do {
70
+ const response = await s3.send(
71
+ new ListObjectsV2Command({
72
+ Bucket: config.bucketName,
73
+ Prefix: prefix || undefined,
74
+ ContinuationToken: continuationToken,
75
+ MaxKeys: 1000,
76
+ })
77
+ );
78
+
79
+ for (const obj of response.Contents || []) {
80
+ const key = obj.Key;
81
+ if (!key || !isMediaFile(key)) continue;
82
+
83
+ // Skip zero-byte "folder marker" objects
84
+ if (!obj.Size) continue;
85
+
86
+ const filename = key.split("/").pop() || key;
87
+ const extension = filename.includes(".")
88
+ ? filename.split(".").pop()!.toLowerCase()
89
+ : "";
90
+
91
+ files.push({
92
+ path: key,
93
+ filename,
94
+ extension,
95
+ file_size: obj.Size,
96
+ mime_type: getMimeType(filename),
97
+ file_hash: obj.ETag?.replace(/"/g, "") || undefined,
98
+ });
99
+ }
100
+
101
+ continuationToken = response.NextContinuationToken;
102
+ } while (continuationToken);
103
+
104
+ // Sort by path for consistent ordering
105
+ files.sort((a, b) => a.path.localeCompare(b.path));
106
+
107
+ return files;
108
+ }
109
+
110
+ /**
111
+ * Resolve a relative path to a direct R2 public URL.
112
+ *
113
+ * This is the key advantage of R2: no API call needed.
114
+ * The URL is deterministic: {bucketUrl}/{relativePath}
115
+ */
116
+ async resolveUrl(relativePath: string): Promise<string> {
117
+ const config = await this.getConfig();
118
+ if (!config.bucketUrl) {
119
+ throw new Error("R2 public bucket URL not configured");
120
+ }
121
+
122
+ const base = config.bucketUrl.replace(/\/$/, "");
123
+ const path = relativePath.replace(/^\//, "");
124
+ return `${base}/${path}`;
125
+ }
126
+
127
+ /**
128
+ * Test R2 connectivity.
129
+ * #29: Tests both public URL reachability AND S3 API credentials.
130
+ */
131
+ async testConnection(): Promise<{ ok: boolean; error?: string }> {
132
+ try {
133
+ const config = await this.getConfig();
134
+ if (!config.bucketUrl) {
135
+ return { ok: false, error: "R2 public URL not configured" };
136
+ }
137
+
138
+ // Test 1: Public URL reachability
139
+ const response = await fetch(config.bucketUrl, {
140
+ method: "HEAD",
141
+ signal: AbortSignal.timeout(5000),
142
+ });
143
+
144
+ if (!response.ok && response.status !== 404) {
145
+ return {
146
+ ok: false,
147
+ error: `R2 bucket returned HTTP ${response.status}`,
148
+ };
149
+ }
150
+
151
+ // #29: Test 2: S3 API credentials by listing one key
152
+ if (config.endpoint && config.accessKeyId && config.secretAccessKey) {
153
+ try {
154
+ const s3 = this.getOrCreateS3Client(config);
155
+ await s3.send(new ListObjectsV2Command({
156
+ Bucket: config.bucketName,
157
+ MaxKeys: 1,
158
+ }));
159
+ } catch (s3Err) {
160
+ return {
161
+ ok: false,
162
+ error: `S3 API test failed: ${s3Err instanceof Error ? s3Err.message : "credential error"}`,
163
+ };
164
+ }
165
+ }
166
+
167
+ return { ok: true };
168
+ } catch (err) {
169
+ return {
170
+ ok: false,
171
+ error: err instanceof Error ? err.message : "Connection test failed",
172
+ };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get R2 connection status for the admin UI.
178
+ */
179
+ async getStatus(): Promise<StorageProviderConfig> {
180
+ try {
181
+ const registry = await client.fetch(
182
+ `*[_type == "assetRegistry"][0]{
183
+ r2_bucket_url,
184
+ r2_bucket_name,
185
+ r2_connected_at
186
+ }`
187
+ );
188
+
189
+ const connected = !!registry?.r2_bucket_url && !!registry?.r2_bucket_name;
190
+
191
+ return {
192
+ provider: "r2",
193
+ connected,
194
+ connectedAt: registry?.r2_connected_at || undefined,
195
+ displayName: connected
196
+ ? `Cloudflare R2 — ${registry?.r2_bucket_name}`
197
+ : "Cloudflare R2 — Not connected",
198
+ };
199
+ } catch {
200
+ return {
201
+ provider: "r2",
202
+ connected: false,
203
+ displayName: "Cloudflare R2 — Status unavailable",
204
+ };
205
+ }
206
+ }
207
+
208
+ // ============================================
209
+ // Internal helpers
210
+ // ============================================
211
+
212
+ /**
213
+ * #18: Get or create a singleton S3Client, reusing it if config hasn't changed.
214
+ */
215
+ private getOrCreateS3Client(config: R2Config): S3Client {
216
+ const hash = `${config.endpoint}:${config.accessKeyId}:${config.bucketName}`;
217
+ if (s3Singleton && s3ConfigHash === hash) return s3Singleton;
218
+
219
+ // Destroy old client if config changed
220
+ if (s3Singleton) {
221
+ try { s3Singleton.destroy(); } catch { /* ignore */ }
222
+ }
223
+
224
+ s3Singleton = new S3Client({
225
+ region: "auto",
226
+ endpoint: config.endpoint,
227
+ credentials: {
228
+ accessKeyId: config.accessKeyId,
229
+ secretAccessKey: config.secretAccessKey,
230
+ },
231
+ });
232
+ s3ConfigHash = hash;
233
+ return s3Singleton;
234
+ }
235
+
236
+ /**
237
+ * Read R2 configuration from the asset registry in Sanity.
238
+ * Decrypts stored credentials.
239
+ * #5: Gracefully handles corrupted encrypted tokens.
240
+ */
241
+ private async getConfig(): Promise<R2Config> {
242
+ const registry = await client.fetch(
243
+ `*[_type == "assetRegistry"][0]{
244
+ r2_bucket_url,
245
+ r2_endpoint,
246
+ r2_bucket_name,
247
+ r2_access_key_id,
248
+ r2_secret_access_key
249
+ }`
250
+ );
251
+
252
+ if (!registry) {
253
+ return {
254
+ bucketUrl: "",
255
+ endpoint: "",
256
+ bucketName: "",
257
+ accessKeyId: "",
258
+ secretAccessKey: "",
259
+ };
260
+ }
261
+
262
+ // #5: Wrap decryptToken in try/catch to prevent crash on corrupted tokens
263
+ let accessKeyId = "";
264
+ let secretAccessKey = "";
265
+ try {
266
+ accessKeyId = registry.r2_access_key_id
267
+ ? await decryptToken(registry.r2_access_key_id)
268
+ : "";
269
+ secretAccessKey = registry.r2_secret_access_key
270
+ ? await decryptToken(registry.r2_secret_access_key)
271
+ : "";
272
+ } catch (err) {
273
+ logger.error("[R2Adapter]", "Failed to decrypt credentials", err);
274
+ // Return config without credentials — callers will see empty strings
275
+ // and handle appropriately (e.g., showing "R2 not connected")
276
+ }
277
+
278
+ return {
279
+ bucketUrl: registry.r2_bucket_url || "",
280
+ endpoint: registry.r2_endpoint || "",
281
+ bucketName: registry.r2_bucket_name || "",
282
+ accessKeyId,
283
+ secretAccessKey,
284
+ };
285
+ }
286
+ }