@morphika/andami 0.1.2

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 +50 -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 +212 -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,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CustomSectionInstanceRenderer — Renders a custom section instance on the public site.
5
+ *
6
+ * Fetches the section data from the cached public API endpoint and renders it
7
+ * using SectionV2Renderer. The API response is CDN-cached for 1 hour.
8
+ *
9
+ * Session 108: Created as part of Custom Sections Phase 2.
10
+ * Session 110: M2 fix — loading skeleton to prevent CLS.
11
+ */
12
+
13
+ import { useEffect, useState } from "react";
14
+ import type { CustomSectionInstance, PageSectionV2 } from "../../lib/sanity/types";
15
+ import SectionV2Renderer from "./SectionV2Renderer";
16
+
17
+ interface CustomSectionInstanceRendererProps {
18
+ instance: CustomSectionInstance;
19
+ }
20
+
21
+ export default function CustomSectionInstanceRenderer({
22
+ instance,
23
+ }: CustomSectionInstanceRendererProps) {
24
+ const [section, setSection] = useState<PageSectionV2 | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState(false);
27
+
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+
31
+ fetch(`/api/custom-sections/${instance.custom_section_id}`)
32
+ .then((res) => {
33
+ if (!res.ok) throw new Error("Not found");
34
+ return res.json();
35
+ })
36
+ .then((data) => {
37
+ if (!cancelled && data.section) {
38
+ setSection(data.section);
39
+ }
40
+ })
41
+ .catch(() => {
42
+ if (!cancelled) setError(true);
43
+ })
44
+ .finally(() => {
45
+ if (!cancelled) setLoading(false);
46
+ });
47
+
48
+ return () => { cancelled = true; };
49
+ }, [instance.custom_section_id]);
50
+
51
+ // Error — render nothing (silent fail, no layout shift)
52
+ if (error) return null;
53
+
54
+ // Loading — minimal skeleton preserving vertical space
55
+ if (loading || !section) {
56
+ return (
57
+ <div
58
+ className="w-full animate-pulse"
59
+ style={{ minHeight: 120, background: "transparent" }}
60
+ aria-hidden="true"
61
+ />
62
+ );
63
+ }
64
+
65
+ // Merge per-instance settings overrides on top of the section's base settings
66
+ let mergedSection: PageSectionV2 = instance.settings_overrides
67
+ ? { ...section, settings: { ...section.settings, ...instance.settings_overrides } }
68
+ : section;
69
+
70
+ // Merge per-instance responsive overrides (viewport-specific spacing/border/etc.)
71
+ if (instance.responsive_overrides) {
72
+ mergedSection = {
73
+ ...mergedSection,
74
+ responsive: {
75
+ ...mergedSection.responsive,
76
+ ...instance.responsive_overrides,
77
+ },
78
+ };
79
+ }
80
+
81
+ return <SectionV2Renderer section={mergedSection} />;
82
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ /**
4
+ * EnterAnimationWrapper — applies enter (reveal) animations on the public site.
5
+ *
6
+ * Replaces ScrollAnimationWrapper (Session 120).
7
+ *
8
+ * Single trigger mode: IntersectionObserver (15% threshold).
9
+ * - Above-the-fold: if already intersecting on mount, plays immediately.
10
+ * - Below-the-fold: waits for scroll to reveal, then plays once.
11
+ *
12
+ * Applies inline @keyframes from getEnterKeyframes() — no CSS animation-timeline,
13
+ * no scroll-linked animation, no Firefox fallback.
14
+ *
15
+ * Respects prefers-reduced-motion: skips animation, shows element immediately.
16
+ * Sets data-entered when complete (for PageExitContext exit animations).
17
+ */
18
+
19
+ import { useRef, useEffect, useState, type CSSProperties, type ReactNode } from "react";
20
+ import type { ResolvedEnterAnimationConfig, EnterPreset } from "../../lib/animation/enter-types";
21
+ import { getEnterKeyframes } from "../../lib/animation/enter-presets";
22
+
23
+ interface EnterAnimationWrapperProps {
24
+ config: ResolvedEnterAnimationConfig;
25
+ children: ReactNode;
26
+ /** HTML tag for the wrapper element */
27
+ as?: "div" | "section";
28
+ className?: string;
29
+ style?: CSSProperties;
30
+ /** Stagger index within parent — offsets animation delay for sequential reveals */
31
+ staggerIndex?: number;
32
+ /** Stagger delay in ms (multiplied by staggerIndex) */
33
+ staggerDelay?: number;
34
+ }
35
+
36
+ /**
37
+ * Map enter preset names to @keyframes names in globals.css.
38
+ */
39
+ const PRESET_KEYFRAME_MAP: Record<string, string> = {
40
+ fade: "enter-fade",
41
+ "slide-up": "enter-slide-up",
42
+ "slide-down": "enter-slide-down",
43
+ scale: "enter-scale",
44
+ blur: "enter-blur",
45
+ "blur-in": "enter-blur-in",
46
+ reveal: "enter-reveal",
47
+ };
48
+
49
+ export default function EnterAnimationWrapper({
50
+ config,
51
+ children,
52
+ as: Tag = "div",
53
+ className,
54
+ style,
55
+ staggerIndex,
56
+ staggerDelay,
57
+ }: EnterAnimationWrapperProps) {
58
+ const { preset, duration, delay, easing } = config;
59
+
60
+ const ref = useRef<HTMLElement>(null);
61
+ const [hasEntered, setHasEntered] = useState(false);
62
+
63
+ // Compute total delay including stagger
64
+ const totalDelay = delay + (staggerIndex !== undefined && staggerDelay ? staggerIndex * staggerDelay : 0);
65
+
66
+ // Get keyframe data for initial hidden state
67
+ const keyframes = getEnterKeyframes(preset);
68
+
69
+ // ── IntersectionObserver: above-the-fold detection + scroll reveal ──
70
+ useEffect(() => {
71
+ const el = ref.current;
72
+ if (!el || hasEntered) return;
73
+
74
+ // Respect prefers-reduced-motion
75
+ if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
76
+ setHasEntered(true);
77
+ return;
78
+ }
79
+
80
+ const observer = new IntersectionObserver(
81
+ ([entry]) => {
82
+ if (entry.isIntersecting) {
83
+ // Whether above-the-fold (already visible on mount) or below-the-fold
84
+ // (scrolled into view), behavior is the same: play animation once.
85
+ setHasEntered(true);
86
+ observer.disconnect();
87
+ }
88
+ },
89
+ { threshold: 0.15 },
90
+ );
91
+
92
+ observer.observe(el);
93
+ return () => observer.disconnect();
94
+ }, [hasEntered]);
95
+
96
+ // If preset is "none" or has no keyframes (typewriter), just render children
97
+ if (preset === "none" || !keyframes) {
98
+ return (
99
+ <Tag className={className} style={style}>
100
+ {children}
101
+ </Tag>
102
+ );
103
+ }
104
+
105
+ const animName = PRESET_KEYFRAME_MAP[preset];
106
+
107
+ // Build styles based on state
108
+ let animStyle: CSSProperties;
109
+
110
+ if (hasEntered) {
111
+ // Play the animation
112
+ animStyle = {
113
+ animationName: animName,
114
+ animationDuration: `${duration}ms`,
115
+ animationDelay: `${totalDelay}ms`,
116
+ animationTimingFunction: easing,
117
+ animationFillMode: "both",
118
+ };
119
+ } else {
120
+ // Initial hidden state from keyframe "from" values
121
+ animStyle = keyframes.from as unknown as CSSProperties;
122
+ }
123
+
124
+ const mergedStyle: CSSProperties = {
125
+ ...style,
126
+ ...animStyle,
127
+ };
128
+
129
+ return (
130
+ <Tag
131
+ ref={ref as React.Ref<HTMLDivElement> & React.Ref<HTMLElement>}
132
+ data-enter-animation={preset}
133
+ data-entered={hasEntered ? "" : undefined}
134
+ className={className}
135
+ style={mergedStyle}
136
+ >
137
+ {children}
138
+ </Tag>
139
+ );
140
+ }
@@ -0,0 +1,308 @@
1
+ "use client";
2
+
3
+ /**
4
+ * HoverAnimationWrapper — applies hover effects to blocks on the public site.
5
+ *
6
+ * Handles both CSS-based hovers (scale-up, lift, tilt-3d, etc.) and
7
+ * shader-based hovers (ripple, rgb-shift, pixelate) via ShaderCanvas.
8
+ *
9
+ * Block-level only — no cascade. Config comes directly from block.hover_effect.
10
+ *
11
+ * Respects:
12
+ * - prefers-reduced-motion: disables all hover effects (returns children unwrapped)
13
+ * - Touch devices: hover effects are naturally absent on touch
14
+ *
15
+ * Session 60, updated Session 64 (throttled tilt-3d, reduced-motion).
16
+ * Session 120: Updated to accept HoverEffectConfig, merged ShaderEffectWrapper.
17
+ * Session 128: Fixed hooks ordering — all hooks must be called before any
18
+ * conditional return to comply with React's Rules of Hooks. The shader path
19
+ * early return was skipping useCallback/useEffect, causing React error #300
20
+ * ("Rendered fewer hooks than expected").
21
+ */
22
+
23
+ import { useRef, useCallback, useEffect, useState, lazy, Suspense, type CSSProperties, type ReactNode, Component, type ErrorInfo } from "react";
24
+ import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
25
+ import { getHoverEffectStyles, isShaderPreset, SMOOTHNESS_VALUES } from "../../lib/animation/hover-effect-presets";
26
+ import type { ResolvedShaderEffectConfig } from "../../lib/animation/hover-effect-types";
27
+ import { getSiteConfig } from "../../lib/config";
28
+
29
+ // Lazy import — zero bundle impact when shaders aren't used.
30
+ // Uses React.lazy + Suspense instead of next/dynamic to avoid potential
31
+ // SSR hydration issues with { ssr: false } in Next.js App Router.
32
+ const ShaderCanvas = lazy(() => import("./ShaderCanvas"));
33
+
34
+ // ── Error boundary for shader rendering ──
35
+ // Catches WebGL / shader errors and falls back to a plain image.
36
+ interface ShaderErrorBoundaryProps {
37
+ fallbackSrc: string;
38
+ fallbackAlt: string;
39
+ children: ReactNode;
40
+ }
41
+ interface ShaderErrorBoundaryState { hasError: boolean }
42
+
43
+ class ShaderErrorBoundary extends Component<ShaderErrorBoundaryProps, ShaderErrorBoundaryState> {
44
+ state: ShaderErrorBoundaryState = { hasError: false };
45
+
46
+ static getDerivedStateFromError(): ShaderErrorBoundaryState {
47
+ return { hasError: true };
48
+ }
49
+
50
+ componentDidCatch(error: Error, info: ErrorInfo) {
51
+ // eslint-disable-next-line no-console
52
+ console.warn("[ShaderErrorBoundary] Caught error, falling back to plain image:", error, info.componentStack);
53
+ }
54
+
55
+ render() {
56
+ if (this.state.hasError) {
57
+ return (
58
+ // eslint-disable-next-line @next/next/no-img-element
59
+ <img
60
+ src={this.props.fallbackSrc}
61
+ alt={this.props.fallbackAlt}
62
+ style={{ display: "block", width: "100%", height: "100%", objectFit: "cover" }}
63
+ />
64
+ );
65
+ }
66
+ return this.props.children;
67
+ }
68
+ }
69
+
70
+ interface HoverAnimationWrapperProps {
71
+ config: HoverEffectConfig;
72
+ children: ReactNode;
73
+ as?: "div" | "section";
74
+ className?: string;
75
+ style?: CSSProperties;
76
+ /**
77
+ * Required for shader presets — the image src to apply the shader to.
78
+ * If not provided and a shader preset is selected, falls back to CSS-only.
79
+ */
80
+ shaderSrc?: string;
81
+ shaderAlt?: string;
82
+ /** Style for the shader container */
83
+ shaderContainerStyle?: CSSProperties;
84
+ shaderContainerClassName?: string;
85
+ shaderImgProps?: React.ImgHTMLAttributes<HTMLImageElement>;
86
+ }
87
+
88
+ // ── Capability checks (memoized) ──
89
+
90
+ let _webglSupported: boolean | null = null;
91
+ function isWebGLSupported(): boolean {
92
+ if (_webglSupported !== null) return _webglSupported;
93
+ try {
94
+ const canvas = document.createElement("canvas");
95
+ const ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
96
+ _webglSupported = !!ctx;
97
+ } catch {
98
+ _webglSupported = false;
99
+ }
100
+ return _webglSupported;
101
+ }
102
+
103
+ function isTouchOnlyDevice(): boolean {
104
+ const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
105
+ if (hasFinePointer) return false;
106
+ const hasCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
107
+ return hasCoarsePointer || "ontouchstart" in window || navigator.maxTouchPoints > 0;
108
+ }
109
+
110
+ export default function HoverAnimationWrapper({
111
+ config,
112
+ children,
113
+ as: Tag = "div",
114
+ className,
115
+ style,
116
+ shaderSrc,
117
+ shaderAlt,
118
+ shaderContainerStyle,
119
+ shaderContainerClassName,
120
+ shaderImgProps,
121
+ }: HoverAnimationWrapperProps) {
122
+ const {
123
+ preset = "none",
124
+ duration = 300,
125
+ easing = "ease-out",
126
+ shader_speed = 1.0,
127
+ shader_smoothness = "normal",
128
+ } = config;
129
+
130
+ const isShader = isShaderPreset(preset);
131
+
132
+ // ── ALL hooks must be called unconditionally (Rules of Hooks) ──
133
+ // These hooks are used by the CSS hover path, but must be called even
134
+ // on the shader path to maintain consistent hook ordering across renders.
135
+
136
+ const ref = useRef<HTMLDivElement>(null);
137
+ const rafId = useRef<number>(0);
138
+ const leaveTimerId = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+
140
+ // Respect prefers-reduced-motion
141
+ const [reducedMotion, setReducedMotion] = useState(false);
142
+ const [canRenderShader, setCanRenderShader] = useState(false);
143
+
144
+ useEffect(() => {
145
+ const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
146
+ setReducedMotion(mql.matches);
147
+ const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches);
148
+ mql.addEventListener("change", handler);
149
+ return () => mql.removeEventListener("change", handler);
150
+ }, []);
151
+
152
+ // Check shader capability once
153
+ useEffect(() => {
154
+ if (!isShaderPreset(preset)) return;
155
+ const siteConfig = getSiteConfig();
156
+ if (!(siteConfig.features as Record<string, boolean>).shaderEffects) {
157
+ setCanRenderShader(false);
158
+ return;
159
+ }
160
+ const webgl = isWebGLSupported();
161
+ const touchOnly = isTouchOnlyDevice();
162
+ const rm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
163
+ setCanRenderShader(webgl && !touchOnly && !rm);
164
+ }, [preset]);
165
+
166
+ // Throttled mouse move handler for tilt-3d preset (rAF-based)
167
+ // Called unconditionally to maintain hook ordering.
168
+ const handleMouseMove = useCallback(
169
+ (e: React.MouseEvent) => {
170
+ if (preset !== "tilt-3d") return;
171
+ const el = ref.current;
172
+ if (!el) return;
173
+
174
+ if (rafId.current) cancelAnimationFrame(rafId.current);
175
+
176
+ const clientX = e.clientX;
177
+ const clientY = e.clientY;
178
+
179
+ rafId.current = requestAnimationFrame(() => {
180
+ const rect = el.getBoundingClientRect();
181
+ const x = (clientX - rect.left) / rect.width;
182
+ const y = (clientY - rect.top) / rect.height;
183
+
184
+ const maxTilt = 8;
185
+ const rotateY = (x - 0.5) * 2 * maxTilt;
186
+ const rotateX = (0.5 - y) * 2 * maxTilt;
187
+
188
+ el.style.transform = `perspective(800px) rotateX(${rotateX.toFixed(1)}deg) rotateY(${rotateY.toFixed(1)}deg)`;
189
+ });
190
+ },
191
+ [preset]
192
+ );
193
+
194
+ const handleMouseLeave = useCallback(() => {
195
+ if (preset !== "tilt-3d") return;
196
+ if (rafId.current) {
197
+ cancelAnimationFrame(rafId.current);
198
+ rafId.current = 0;
199
+ }
200
+ if (leaveTimerId.current) clearTimeout(leaveTimerId.current);
201
+ leaveTimerId.current = setTimeout(() => {
202
+ const el = ref.current;
203
+ if (!el) return;
204
+ el.style.transform = "perspective(800px) rotateX(0deg) rotateY(0deg)";
205
+ }, 50);
206
+ }, [preset]);
207
+
208
+ // Cleanup rAF and debounce timer on unmount
209
+ useEffect(() => {
210
+ return () => {
211
+ if (rafId.current) cancelAnimationFrame(rafId.current);
212
+ if (leaveTimerId.current) clearTimeout(leaveTimerId.current);
213
+ };
214
+ }, []);
215
+
216
+ // ── All hooks called. Conditional returns are now safe. ──
217
+
218
+ // Shader preset without shaderSrc: passthrough (no wrapper div, no CSS hover)
219
+ if (isShader && !shaderSrc) {
220
+ return <>{children}</>;
221
+ }
222
+
223
+ // ── Shader preset rendering ──
224
+ // Renders children normally (for layout/dimensions) and overlays ShaderCanvas on top.
225
+ // Children provide the natural size; the shader canvas fills the container absolutely.
226
+ if (isShader && shaderSrc && canRenderShader) {
227
+ const shaderConfig: ResolvedShaderEffectConfig = {
228
+ preset: preset,
229
+ trigger: "hover",
230
+ intensity: 1.0,
231
+ speed: shader_speed,
232
+ smoothing: SMOOTHNESS_VALUES[shader_smoothness] ?? 0.05,
233
+ };
234
+
235
+ // Fallback image shown while ShaderCanvas loads or if it fails
236
+ const fallbackImg = (
237
+ // eslint-disable-next-line @next/next/no-img-element
238
+ <img
239
+ src={shaderSrc}
240
+ alt={shaderAlt ?? ""}
241
+ style={{
242
+ display: "block",
243
+ width: "100%",
244
+ height: "100%",
245
+ objectFit: "cover",
246
+ }}
247
+ />
248
+ );
249
+
250
+ return (
251
+ <div
252
+ style={{ position: "relative", overflow: "hidden", ...shaderContainerStyle }}
253
+ className={shaderContainerClassName || className}
254
+ >
255
+ {/* Children provide layout dimensions (padding, border-radius, natural image size) */}
256
+ <div aria-hidden="true" style={{ visibility: "hidden" }}>
257
+ {children}
258
+ </div>
259
+ {/* Shader canvas fills the container on top of the hidden children */}
260
+ <ShaderErrorBoundary fallbackSrc={shaderSrc} fallbackAlt={shaderAlt ?? ""}>
261
+ <Suspense fallback={fallbackImg}>
262
+ <ShaderCanvas
263
+ src={shaderSrc}
264
+ config={shaderConfig}
265
+ style={{ position: "absolute", inset: 0 }}
266
+ />
267
+ </Suspense>
268
+ </ShaderErrorBoundary>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ // ── CSS hover preset rendering ──
274
+
275
+ const hoverStyles = getHoverEffectStyles(preset, duration, easing);
276
+
277
+ // When reduced motion is preferred or shader preset falls through (canRenderShader=false),
278
+ // render children without any animation wrapper
279
+ if (reducedMotion || !hoverStyles) {
280
+ return (
281
+ <Tag className={className} style={style}>
282
+ {children}
283
+ </Tag>
284
+ );
285
+ }
286
+
287
+ // Merge base styles with user styles
288
+ const mergedStyle: CSSProperties = {
289
+ ...style,
290
+ ...(hoverStyles.base as unknown as CSSProperties),
291
+ transition: `all ${duration}ms ${easing}`,
292
+ } as CSSProperties;
293
+
294
+ const isTilt = preset === "tilt-3d";
295
+
296
+ return (
297
+ <Tag
298
+ ref={ref as React.Ref<HTMLDivElement>}
299
+ data-hover-effect={preset}
300
+ className={className}
301
+ style={mergedStyle}
302
+ onMouseMove={isTilt ? handleMouseMove : undefined}
303
+ onMouseLeave={isTilt ? handleMouseLeave : undefined}
304
+ >
305
+ {children}
306
+ </Tag>
307
+ );
308
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import type { ImageBlock } from "../../lib/sanity/types";
4
+ import { useAssetUrl } from "../../lib/contexts/AssetContext";
5
+ import { handleImageRetry } from "../../lib/asset-retry";
6
+
7
+ /**
8
+ * Width handling — must match builder LiveImage in BlockLivePreview.tsx.
9
+ * Builder uses: full=100%, contained=75%, small=50% with margin auto.
10
+ */
11
+ const widthStyleMap: Record<string, { width: string; margin?: string }> = {
12
+ full: { width: "100%" },
13
+ contained: { width: "75%", margin: "0 auto" },
14
+ small: { width: "50%", margin: "0 auto" },
15
+ };
16
+
17
+ const aspectMap: Record<string, string | undefined> = {
18
+ auto: undefined,
19
+ "16:9": "16/9",
20
+ "4:3": "4/3",
21
+ "1:1": "1/1",
22
+ "21:9": "21/9",
23
+ };
24
+
25
+ export default function ImageBlockRenderer({ block }: { block: ImageBlock }) {
26
+ const resolveAsset = useAssetUrl();
27
+ const src = resolveAsset(block.asset_path);
28
+ const widthStyle = widthStyleMap[block.width ?? "full"] || widthStyleMap.full;
29
+ const aspect = aspectMap[block.aspect_ratio ?? "auto"];
30
+
31
+ const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
32
+
33
+ const imgStyle: React.CSSProperties = {
34
+ width: "100%",
35
+ display: "block",
36
+ objectFit: aspect ? "cover" : undefined,
37
+ aspectRatio: aspect,
38
+ };
39
+
40
+ const imgClassName = block.shadow ? "shadow-lg" : "";
41
+
42
+ return (
43
+ <figure style={{ ...widthStyle, borderRadius, overflow: "hidden" }}>
44
+ {/* eslint-disable-next-line @next/next/no-img-element */}
45
+ <img
46
+ src={src}
47
+ alt={block.alt ?? ""}
48
+ loading={block.lazy !== false ? "lazy" : "eager"}
49
+ decoding="async"
50
+ onError={handleImageRetry}
51
+ style={imgStyle}
52
+ className={imgClassName}
53
+ />
54
+ {block.caption && (
55
+ <figcaption className="mt-2 font-mono text-xs uppercase tracking-wider text-brand-muted">
56
+ {block.caption}
57
+ </figcaption>
58
+ )}
59
+ </figure>
60
+ );
61
+ }