@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,392 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ShaderCanvas — OGL WebGL canvas component.
5
+ *
6
+ * Renders a full-size canvas with a fragment shader applied to an image texture.
7
+ * Handles mouse tracking, scroll progress, lerp smoothing, IntersectionObserver
8
+ * visibility pausing, and clean lifecycle.
9
+ *
10
+ * This component is only mounted on the public site (never in the builder).
11
+ *
12
+ * Session 71.
13
+ */
14
+
15
+ import { useEffect, useRef, useCallback, useState } from "react";
16
+ import type { ResolvedShaderEffectConfig } from "../../lib/animation/hover-effect-types";
17
+ import { getFragmentShader, vertexShader } from "../../lib/shader/glsl";
18
+
19
+ // ── Lerp helper ─────────────────────────────────────────────────────
20
+
21
+ function lerp(a: number, b: number, t: number): number {
22
+ return a + (b - a) * t;
23
+ }
24
+
25
+ // ── Max simultaneous WebGL contexts ─────────────────────────────────
26
+
27
+ let activeContextCount = 0;
28
+ const MAX_CONTEXTS = 6;
29
+
30
+ // ── Component ───────────────────────────────────────────────────────
31
+
32
+ interface ShaderCanvasProps {
33
+ src: string;
34
+ config: ResolvedShaderEffectConfig;
35
+ className?: string;
36
+ style?: React.CSSProperties;
37
+ }
38
+
39
+ export default function ShaderCanvas({ src, config, className, style }: ShaderCanvasProps) {
40
+ const containerRef = useRef<HTMLDivElement>(null);
41
+ const canvasRef = useRef<HTMLCanvasElement>(null);
42
+ const rafRef = useRef<number>(0);
43
+ const rendererRef = useRef<unknown>(null);
44
+ const isVisibleRef = useRef(true);
45
+ const contextClaimedRef = useRef(false);
46
+ /** Track observers created inside the async .then() for cleanup */
47
+ const observersRef = useRef<{ io?: IntersectionObserver; ro?: ResizeObserver }>({});
48
+ /** If true, shader init failed — component renders plain image fallback */
49
+ const [shaderFailed, setShaderFailed] = useState(false);
50
+
51
+ // Mutable state for animation loop (avoid re-renders)
52
+ const mouseRef = useRef({ x: 0.5, y: 0.5 });
53
+ const targetMouseRef = useRef({ x: 0.5, y: 0.5 });
54
+ const hoverRef = useRef(0);
55
+ const targetHoverRef = useRef(0);
56
+ const scrollProgressRef = useRef(0);
57
+ const targetScrollRef = useRef(0);
58
+ const startTimeRef = useRef(0);
59
+
60
+ // ── Mouse handlers ──────────────────────────────────────────────
61
+
62
+ const onMouseMove = useCallback((e: MouseEvent) => {
63
+ const canvas = canvasRef.current;
64
+ if (!canvas) return;
65
+ const rect = canvas.getBoundingClientRect();
66
+ targetMouseRef.current = {
67
+ x: (e.clientX - rect.left) / rect.width,
68
+ y: 1.0 - (e.clientY - rect.top) / rect.height, // Flip Y for GL
69
+ };
70
+ }, []);
71
+
72
+ const onMouseEnter = useCallback(() => {
73
+ targetHoverRef.current = 1;
74
+ }, []);
75
+
76
+ const onMouseLeave = useCallback(() => {
77
+ targetHoverRef.current = 0;
78
+ }, []);
79
+
80
+ // ── Scroll handler (for scroll trigger) ─────────────────────────
81
+
82
+ const onScroll = useCallback(() => {
83
+ const canvas = canvasRef.current;
84
+ if (!canvas) return;
85
+ const rect = canvas.getBoundingClientRect();
86
+ const viewportH = window.innerHeight;
87
+ // 0 = element just entered bottom, 1 = element just left top
88
+ const progress = 1 - (rect.top + rect.height) / (viewportH + rect.height);
89
+ targetScrollRef.current = Math.max(0, Math.min(1, progress));
90
+ }, []);
91
+
92
+ // ── Main effect: init OGL, animation loop, cleanup ──────────────
93
+
94
+ useEffect(() => {
95
+ const canvas = canvasRef.current;
96
+ const container = containerRef.current;
97
+ if (!canvas || !container) return;
98
+
99
+ // Context limit check
100
+ if (activeContextCount >= MAX_CONTEXTS) return;
101
+ activeContextCount++;
102
+ contextClaimedRef.current = true;
103
+
104
+ // Dynamic import OGL (tree-shaken)
105
+ let destroyed = false;
106
+
107
+ import("ogl").then(({ Renderer, Program, Mesh, Texture, Triangle }) => {
108
+ if (destroyed || !canvas) return;
109
+
110
+ try {
111
+ // ── Helper: set renderer size without killing CSS layout ────
112
+ // OGL's setSize() overwrites canvas.style.width/height with px values,
113
+ // which destroys the CSS `width:100%; height:100%` that makes the canvas
114
+ // fill its container. We restore CSS immediately after.
115
+ function setRendererSize(renderer: { setSize: (w: number, h: number) => void }, w: number, h: number) {
116
+ renderer.setSize(w, h);
117
+ // Restore CSS so the canvas stays responsive
118
+ const c = canvasRef.current;
119
+ if (c) {
120
+ c.style.width = "100%";
121
+ c.style.height = "100%";
122
+ }
123
+ }
124
+
125
+ // ── Init renderer ───────────────────────────────────────────
126
+ const dpr = Math.min(window.devicePixelRatio, 2);
127
+ const renderer = new Renderer({
128
+ canvas,
129
+ dpr,
130
+ alpha: false,
131
+ antialias: false,
132
+ powerPreference: "low-power",
133
+ });
134
+ rendererRef.current = renderer;
135
+ const gl = renderer.gl;
136
+
137
+ // Size to container (measure the wrapper div, not the canvas)
138
+ const rect = container.getBoundingClientRect();
139
+ setRendererSize(renderer, rect.width, rect.height);
140
+
141
+ // ── Load texture from image ─────────────────────────────────
142
+ // Use fetch → blob → object URL to avoid CORS + redirect issues.
143
+ // Both asset proxies (/api/assets/ and /api/admin/assets/) normally
144
+ // return a 302 redirect to R2, which fails in WebGL because the
145
+ // cross-origin target lacks CORS headers. The ?texture=1 parameter
146
+ // tells the proxy to stream actual image bytes (same-origin response),
147
+ // avoiding CORS entirely. This covers both public and preview modes.
148
+ const texture = new Texture(gl);
149
+ const needsTextureParam =
150
+ src.startsWith("/api/assets/") ||
151
+ src.startsWith("/api/admin/assets/");
152
+ const textureSrc = needsTextureParam
153
+ ? `${src}${src.includes("?") ? "&" : "?"}texture=1`
154
+ : src;
155
+ fetch(textureSrc)
156
+ .then((res) => {
157
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
158
+ return res.blob();
159
+ })
160
+ .then((blob) => {
161
+ if (destroyed) return;
162
+ const objectUrl = URL.createObjectURL(blob);
163
+ const img = new Image();
164
+ img.onload = () => {
165
+ if (destroyed) {
166
+ URL.revokeObjectURL(objectUrl);
167
+ return;
168
+ }
169
+ texture.image = img;
170
+ // Revoke after a short delay to ensure OGL has uploaded the texture
171
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 100);
172
+ };
173
+ img.onerror = () => {
174
+ URL.revokeObjectURL(objectUrl);
175
+ // eslint-disable-next-line no-console
176
+ console.warn("[ShaderCanvas] Blob image decode failed");
177
+ setShaderFailed(true);
178
+ };
179
+ img.src = objectUrl;
180
+ })
181
+ .catch((err) => {
182
+ if (destroyed) return;
183
+ // eslint-disable-next-line no-console
184
+ console.warn("[ShaderCanvas] Texture fetch failed, falling back to plain image:", err);
185
+ setShaderFailed(true);
186
+ });
187
+
188
+ // ── Fragment shader ─────────────────────────────────────────
189
+ const fragmentSource = getFragmentShader(config.preset);
190
+ if (!fragmentSource) {
191
+ // eslint-disable-next-line no-console
192
+ console.warn(`[ShaderCanvas] No fragment shader for preset "${config.preset}"`);
193
+ return;
194
+ }
195
+
196
+ // ── Program ─────────────────────────────────────────────────
197
+ const program = new Program(gl, {
198
+ vertex: vertexShader,
199
+ fragment: fragmentSource,
200
+ uniforms: {
201
+ uTexture: { value: texture },
202
+ uMouse: { value: [0.5, 0.5] },
203
+ uIntensity: { value: config.intensity },
204
+ uSpeed: { value: config.speed },
205
+ uTime: { value: 0 },
206
+ uHover: { value: 0 },
207
+ uResolution: { value: [rect.width * dpr, rect.height * dpr] },
208
+ },
209
+ });
210
+
211
+ // ── Geometry (fullscreen triangle) ──────────────────────────
212
+ const geometry = new Triangle(gl);
213
+ const mesh = new Mesh(gl, { geometry, program });
214
+
215
+ // ── Mouse events (hover trigger) ────────────────────────────
216
+ if (config.trigger === "hover") {
217
+ canvas.addEventListener("mousemove", onMouseMove);
218
+ canvas.addEventListener("mouseenter", onMouseEnter);
219
+ canvas.addEventListener("mouseleave", onMouseLeave);
220
+ }
221
+
222
+ // ── Scroll events (scroll trigger) ──────────────────────────
223
+ if (config.trigger === "scroll") {
224
+ window.addEventListener("scroll", onScroll, { passive: true });
225
+ onScroll(); // Initial position
226
+ targetHoverRef.current = 1; // Always "active" for scroll
227
+ }
228
+
229
+ // ── IntersectionObserver: pause when off-screen ─────────────
230
+ const observer = new IntersectionObserver(
231
+ ([entry]) => {
232
+ isVisibleRef.current = entry.isIntersecting;
233
+ },
234
+ { threshold: 0 }
235
+ );
236
+ observer.observe(canvas);
237
+ observersRef.current.io = observer;
238
+
239
+ // ── Resize observer — watches the CONTAINER, not the canvas ─
240
+ // (OGL's setSize overwrites canvas CSS dimensions, so the canvas
241
+ // itself won't trigger resize events after init)
242
+ const resizeObserver = new ResizeObserver(([entry]) => {
243
+ if (destroyed) return;
244
+ const { width, height } = entry.contentRect;
245
+ if (width > 0 && height > 0) {
246
+ setRendererSize(renderer, width, height);
247
+ program.uniforms.uResolution.value = [width * dpr, height * dpr];
248
+ }
249
+ });
250
+ resizeObserver.observe(container);
251
+ observersRef.current.ro = resizeObserver;
252
+
253
+ // ── Animation loop ──────────────────────────────────────────
254
+ startTimeRef.current = performance.now();
255
+ const smoothing = config.smoothing;
256
+
257
+ function animate() {
258
+ if (destroyed) return;
259
+ rafRef.current = requestAnimationFrame(animate);
260
+
261
+ // Skip rendering when off-screen
262
+ if (!isVisibleRef.current) return;
263
+
264
+ try {
265
+ const now = performance.now();
266
+ const elapsed = (now - startTimeRef.current) / 1000;
267
+
268
+ // Lerp mouse position
269
+ mouseRef.current.x = lerp(mouseRef.current.x, targetMouseRef.current.x, smoothing);
270
+ mouseRef.current.y = lerp(mouseRef.current.y, targetMouseRef.current.y, smoothing);
271
+
272
+ // Lerp hover state
273
+ hoverRef.current = lerp(hoverRef.current, targetHoverRef.current, smoothing);
274
+
275
+ // Lerp scroll progress
276
+ if (config.trigger === "scroll") {
277
+ scrollProgressRef.current = lerp(
278
+ scrollProgressRef.current,
279
+ targetScrollRef.current,
280
+ smoothing
281
+ );
282
+ }
283
+
284
+ // Update uniforms
285
+ const u = program.uniforms;
286
+ u.uMouse.value = [mouseRef.current.x, mouseRef.current.y];
287
+ u.uTime.value = elapsed;
288
+
289
+ if (config.trigger === "scroll") {
290
+ // For scroll trigger, hover uniform carries scroll progress
291
+ u.uHover.value = scrollProgressRef.current;
292
+ } else {
293
+ u.uHover.value = hoverRef.current;
294
+ }
295
+
296
+ renderer.render({ scene: mesh });
297
+ } catch {
298
+ // Render error (e.g. lost WebGL context) — stop animation loop gracefully
299
+ destroyed = true;
300
+ }
301
+ }
302
+
303
+ rafRef.current = requestAnimationFrame(animate);
304
+ } catch (err) {
305
+ // Shader compilation, WebGL context creation, or OGL init failure.
306
+ // Fall back to plain image — never crash the page.
307
+ // eslint-disable-next-line no-console
308
+ console.warn("[ShaderCanvas] WebGL init failed, falling back to plain image:", err);
309
+ if (contextClaimedRef.current) {
310
+ activeContextCount--;
311
+ contextClaimedRef.current = false;
312
+ }
313
+ setShaderFailed(true);
314
+ }
315
+ }).catch((err) => {
316
+ // OGL dynamic import failure (e.g. network error, missing module)
317
+ // eslint-disable-next-line no-console
318
+ console.warn("[ShaderCanvas] Failed to load OGL:", err);
319
+ if (contextClaimedRef.current) {
320
+ activeContextCount--;
321
+ contextClaimedRef.current = false;
322
+ }
323
+ setShaderFailed(true);
324
+ });
325
+
326
+ return () => {
327
+ destroyed = true;
328
+ cancelAnimationFrame(rafRef.current);
329
+
330
+ if (contextClaimedRef.current) {
331
+ activeContextCount--;
332
+ contextClaimedRef.current = false;
333
+ }
334
+
335
+ // Disconnect observers created inside .then()
336
+ observersRef.current.io?.disconnect();
337
+ observersRef.current.ro?.disconnect();
338
+ observersRef.current = {};
339
+
340
+ const canvas = canvasRef.current;
341
+ if (canvas) {
342
+ canvas.removeEventListener("mousemove", onMouseMove);
343
+ canvas.removeEventListener("mouseenter", onMouseEnter);
344
+ canvas.removeEventListener("mouseleave", onMouseLeave);
345
+
346
+ // Attempt to lose context cleanly
347
+ const gl = canvas.getContext("webgl");
348
+ if (gl) {
349
+ const ext = gl.getExtension("WEBGL_lose_context");
350
+ if (ext) ext.loseContext();
351
+ }
352
+ }
353
+
354
+ window.removeEventListener("scroll", onScroll);
355
+ };
356
+ // eslint-disable-next-line react-hooks/exhaustive-deps
357
+ }, [src, config.preset, config.trigger, config.intensity, config.speed, config.smoothing]);
358
+
359
+ // If shader init failed, render plain image instead of crashing the page
360
+ if (shaderFailed) {
361
+ return (
362
+ <div className={className} style={{ width: "100%", height: "100%", ...style }}>
363
+ <img
364
+ src={src}
365
+ alt=""
366
+ style={{ display: "block", width: "100%", height: "100%", objectFit: "cover" }}
367
+ />
368
+ </div>
369
+ );
370
+ }
371
+
372
+ return (
373
+ <div
374
+ ref={containerRef}
375
+ className={className}
376
+ style={{
377
+ width: "100%",
378
+ height: "100%",
379
+ ...style,
380
+ }}
381
+ >
382
+ <canvas
383
+ ref={canvasRef}
384
+ style={{
385
+ display: "block",
386
+ width: "100%",
387
+ height: "100%",
388
+ }}
389
+ />
390
+ </div>
391
+ );
392
+ }
@@ -0,0 +1,17 @@
1
+ import type { SpacerBlock } from "../../lib/sanity/types";
2
+
3
+ const heightMap: Record<string, string> = {
4
+ small: "h-8",
5
+ medium: "h-16",
6
+ large: "h-24",
7
+ xlarge: "h-40",
8
+ };
9
+
10
+ export default function SpacerBlockRenderer({ block }: { block: SpacerBlock }) {
11
+ if (block.height === "custom" && block.custom_height) {
12
+ return <div style={{ height: `${block.custom_height}px` }} aria-hidden />;
13
+ }
14
+ return (
15
+ <div className={heightMap[block.height ?? "medium"]} aria-hidden />
16
+ );
17
+ }
@@ -0,0 +1,87 @@
1
+ import type { TextBlock } from "../../lib/sanity/types";
2
+ import { PortableText } from "next-sanity";
3
+
4
+ /** Resolve fontSize: supports numeric px and legacy string enum */
5
+ function resolvePublicFontSize(fontSize?: number | string): string | undefined {
6
+ if (typeof fontSize === "number") return `${fontSize}px`;
7
+ // Legacy Tailwind class mapping
8
+ const tailwindMap: Record<string, string> = {
9
+ small: "text-sm",
10
+ base: "text-base",
11
+ large: "text-lg",
12
+ xl: "text-xl",
13
+ "2xl": "text-2xl",
14
+ "3xl": "text-3xl",
15
+ };
16
+ return tailwindMap[fontSize || "base"];
17
+ }
18
+
19
+ /** Resolve fontWeight: supports string numbers and legacy names */
20
+ function resolvePublicFontWeight(fw?: string): string | undefined {
21
+ if (!fw) return "font-normal";
22
+ const num = parseInt(fw, 10);
23
+ if (!isNaN(num)) {
24
+ // Use inline style instead of Tailwind for numeric weights
25
+ return undefined;
26
+ }
27
+ const twMap: Record<string, string> = {
28
+ normal: "font-normal",
29
+ medium: "font-medium",
30
+ bold: "font-bold",
31
+ };
32
+ return twMap[fw] || "font-normal";
33
+ }
34
+
35
+ const alignmentMap: Record<string, string> = {
36
+ left: "text-left",
37
+ center: "text-center",
38
+ right: "text-right",
39
+ justify: "text-justify",
40
+ };
41
+
42
+ /**
43
+ * Extract CSS class and inline style from a TextBlock's style settings.
44
+ * Shared between TextBlockRenderer (rich text) and TypewriterWrapper (plain text phase).
45
+ */
46
+ export function getTextBlockStyles(block: TextBlock): { className: string; style: React.CSSProperties } {
47
+ const s = block.style;
48
+ const isNumericFontSize = typeof s?.fontSize === "number";
49
+ const isNumericWeight = s?.fontWeight && !isNaN(parseInt(s.fontWeight, 10));
50
+
51
+ const classes = [
52
+ "font-mono",
53
+ !isNumericFontSize ? resolvePublicFontSize(s?.fontSize) : undefined,
54
+ alignmentMap[s?.alignment ?? "left"],
55
+ !isNumericWeight ? resolvePublicFontWeight(s?.fontWeight) : undefined,
56
+ ]
57
+ .filter(Boolean)
58
+ .join(" ");
59
+
60
+ const inlineStyle: React.CSSProperties = {};
61
+ if (isNumericFontSize) inlineStyle.fontSize = `${s!.fontSize}px`;
62
+ if (isNumericWeight) inlineStyle.fontWeight = parseInt(s!.fontWeight!, 10);
63
+ if (s?.color) inlineStyle.color = s.color;
64
+ if (s?.lineHeight) inlineStyle.lineHeight = s.lineHeight;
65
+ if (s?.letterSpacing) inlineStyle.letterSpacing = s.letterSpacing;
66
+ if (s?.maxWidth) inlineStyle.maxWidth = s.maxWidth;
67
+ if (s?.opacity != null) inlineStyle.opacity = s.opacity;
68
+ if (s?.textTransform && s.textTransform !== "none") inlineStyle.textTransform = s.textTransform;
69
+ if (block.columns && block.columns > 1) {
70
+ inlineStyle.columnCount = block.columns;
71
+ inlineStyle.columnGap = "var(--grid-gutter, 24px)";
72
+ }
73
+
74
+ return { className: classes, style: inlineStyle };
75
+ }
76
+
77
+ export default function TextBlockRenderer({ block }: { block: TextBlock }) {
78
+ if (!block.text?.length) return null;
79
+
80
+ const { className, style } = getTextBlockStyles(block);
81
+
82
+ return (
83
+ <div className={`${className} space-y-[0.75em]`} style={style}>
84
+ <PortableText value={block.text} />
85
+ </div>
86
+ );
87
+ }